I have a React frontend using Apollo Client to interact with a backend GraphQL server. The GraphQL server is a Node.js application utilizing Express and Apollo Server, and it communicates with databases via Hasura API. Both the frontend and the GraphQL server are deployed on separate Heroku apps.
A few weeks ago, a small portion of users started complaining that they couldn't log into their accounts. It turned out that the frontend on their devices couldn't connect to the backend GraphQL server. Sentry signals errors like "Failed to fetch" or "Load failed." The Heroku log for the GraphQL backend server doesn't show any record of the failed request.
These users are unable to use the app continuously for several days or weeks. Several users reported that after changing networks (e.g., disconnecting from WiFi), they were able to log in and use the app normally again.
This affects a small percentage of users from different parts of the world. I haven't been able to replicate the error.
Frontend index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./components/App";
import { ApolloProvider } from "@apollo/client";
import { GoogleOAuthProvider } from "@react-oauth/google";
import HttpsRedirect from "react-https-redirect";
import { Provider } from "react-redux";
import store from "./store";
import client from "./client";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<HttpsRedirect>
<Provider store={store}>
<ApolloProvider client={client}>
<GoogleOAuthProvider clientId="xxx">
>
<App />
</GoogleOAuthProvider>
</ApolloProvider>
</Provider>
</HttpsRedirect>
);
Frontend client.js
import {
ApolloClient,
InMemoryCache,
ApolloLink,
from,
HttpLink,
split,
} from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { AUTH_TOKEN, AUTH_WS_TOKEN, GRAPHQL_SERVER_URL } from "./constants";
const token = localStorage.getItem(AUTH_TOKEN);
const authorizationHeader = token ? `Bearer ${token}` : null;
const authLink = new ApolloLink((operation, forward) => {
operation.setContext(({ headers }) => ({
headers: {
authorization: authorizationHeader,
...headers,
},
}));
return forward(operation);
});
const httpLink = new HttpLink({
uri: GRAPHQL_SERVER_URL,
});
const wsLink = new GraphQLWsLink(
createClient({
url: "wss://myhasuraendpoint.hasura.app/v1/graphql",
connectionParams: async () => {
const wsToken = localStorage.getItem(AUTH_WS_TOKEN);
return wsToken
? {
headers: {
Authorization: `Bearer ${wsToken}`,
},
}
: {};
},
})
);
const linkChain = from([authLink, httpLink]);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
linkChain
);
export default new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
});
Backend index.js
import "dotenv/config.js";
import { ApolloServer } from "@apollo/server";
import { GraphQLClient } from "graphql-request";
import jwt from "jsonwebtoken";
import { expressMiddleware } from "@apollo/server/express4";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import { ApolloServerPluginLandingPageDisabled } from "@apollo/server/plugin/disabled";
import express from "express";
import http from "http";
import cors from "cors";
import bodyParser from "body-parser";
import GraphQLJSON from "graphql-type-json";
import { typeDefs } from "./typeDefs.js";
const resolvers = {
JSON: GraphQLJSON,
Query: {
...myResolvers
},
Mutation: {
...myResolvers
},
};
const authKey = "Bearer " + process.env.HASURA_APOLLO_GRAPHQL_JWT;
const headers = {
Authorization: authKey,
};
const client = new GraphQLClient(
"https://myhasuraendpoint.hasura.app/v1/graphql",
{ headers }
);
const getUser = (token) => {
try {
if (token) {
return jwt.verify(token, process.env.S_KEY);
}
return null;
} catch (error) {
return null;
}
};
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginLandingPageDisabled(),
],
introspection: false,
});
await server.start();
app.use(
"/",
cors({
origin: [
"https://www.myfrontendwebsite.com",
"https://myfrontendwebsite.com",
],
}),
bodyParser.json({ limit: "50mb" }),
expressMiddleware(server, {
context: async ({ req, res }) => {
const token = req.get("Authorization") || "";
return { client, user: getUser(token.replace("Bearer ", "")) };
},
})
);
await new Promise((resolve) =>
httpServer.listen({ port: process.env.PORT || 4002 }, resolve)
);
console.log(`🚀 Server ready`);
Initially, I thought the issue might be related to CORS. I tried changing the CORS settings to
cors({
origin: true,
}),
but nothing changed.
Any ideas on how to diagnose the issue or what to try?