as-app/src/network/errorLink.js

139 lines
3.8 KiB
JavaScript

import * as Sentry from "@sentry/react-native";
import { fromPromise } from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { createLogger } from "~/lib/logger";
import { NETWORK_SCOPES } from "~/lib/logger/scopes";
import getStatusCode from "./getStatusCode";
import isAbortError from "./isAbortError";
let pendingRequests = [];
const resolvePendingRequests = () => {
pendingRequests.map((callback) => callback());
pendingRequests = [];
};
import network from "./index";
export default function createErrorLink({ store }) {
const { authActions, getAuthState } = store;
const errorLogger = createLogger({
module: NETWORK_SCOPES.APOLLO,
service: "error-handler",
});
// Helper to extract error message from all possible locations
const getErrorMessage = (error) =>
error.message ||
error.networkError?.message ||
error.graphQLErrors?.[0]?.message ||
"no message available";
// Helper to handle WebSocket and network errors
const handleWebSocketError = (error) => {
const errorMsg = getErrorMessage(error);
if (errorMsg?.includes("Expected HTTP 101")) {
errorLogger.warn("WebSocket upgrade failed", {
reason: "HTTP 101 expected",
error: errorMsg,
});
return true;
}
if (errorMsg?.includes("client is null")) {
errorLogger.warn("WebSocket client not initialized", {
error: errorMsg,
action: "attempting restart",
});
// Attempt to restart the WebSocket connection using the network singleton
network.apolloClient?.restartWS();
errorLogger.info("WebSocket connection restart initiated");
return true;
}
if (errorMsg?.includes("Network request failed")) {
errorLogger.warn("Network request failed", {
reason: "connection may be offline",
error: errorMsg,
});
return true;
}
return false;
};
const errorLink = onError((error) => {
const { operation, forward } = error;
const statusCode = getStatusCode(error);
if (
operation.operationName === "registerUser" ||
operation.operationName === "loginUserToken"
) {
switch (statusCode) {
case 410:
authActions.logout();
break;
default:
// Handle WebSocket errors first
if (handleWebSocketError(error)) {
return;
}
errorLogger.error("Apollo operation error", {
operation: operation.operationName,
error: error.graphQLErrors?.[0]?.message || error,
});
break;
}
return;
}
switch (statusCode) {
case 401:
let forward$;
if (!getAuthState().loading) {
forward$ = fromPromise(
authActions
.reload()
.then((ok) => {
resolvePendingRequests();
return ok;
})
.catch((_error) => {
errorLogger.error("Auth refresh failed", { error: _error });
return;
}),
).filter((value) => Boolean(value));
} else {
forward$ = fromPromise(
new Promise((resolve) => {
pendingRequests.push(() => resolve());
}),
);
}
return forward$.flatMap(() => forward(operation));
default:
if (isAbortError(error)) {
// ignore abort error
return;
}
// Handle WebSocket errors first
if (handleWebSocketError(error)) {
return;
}
// Capture all other errors in Sentry
const errorMessage = `apollo error: ${getErrorMessage(error)}`;
Sentry.captureException(new Error(errorMessage), {
extra: {
errorObject: error,
},
});
}
});
return errorLink;
}