139 lines
3.8 KiB
JavaScript
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;
|
|
}
|