Handling apollo graphql errors globally and render custom ErrorHandler component on error - react-redux

Need to handle Apollo graphql errors globally in client side and render custom ErrorHandler component on error . So I used Apollo's afterware and apollo-link-error
import ApolloClient from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error'
const httpLink = new HttpLink({ uri: '/graphql' });
const logoutLink = onError(({ networkError }) => {
if (networkError.statusCode === 401) {
//need to dispatch a redux action so that ErrorHandler component renders
}
})
const client = new ApolloClient({
link: logoutLink.concat(httpLink),
});
My solution for this (which I guess, is not the correct approach)
import ApolloClient from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { render } from 'react-dom';
import ErrorHandler from '../utils/ErrorHandler';
const httpLink = new HttpLink({ uri: '/graphql' });
const logoutLink = onError(({ networkError }) => {
if (networkError.statusCode === 401) {
const targetDiv = document.getElementById('serviceErrorHandler');
render(
<ErrorHandler message={networkError.message}/>,
targetDiv
);
}
})
const client = new ApolloClient({
link: logoutLink.concat(httpLink),
});
Please suggest an approach for my scenario. Thanks in advance

Had this same problem, one solution we went with was making a function that returns onError and take a parameter (the store):
const errorHandler = store => onError((errors) => {
if (errors.networkError) {
store.dispatch(createApolloErrorAction({
message: GENERIC_ERROR_FETCHING_STRING,
}));
}
});
And use it in a wrapper functions to pass in the store:
let apolloClient = null;
export const createApolloClient = (reduxStore) => {
const cache = new InMemoryCache();
const link = errorHandler(reduxStore).concat(otherLink);
apolloClient = new ApolloClient({ link });
return apolloClient;
};
export const getApolloClient = () => apolloClient;

Related

How can you have both Authentication and Subscriptions with Apollo Client

I'm pretty new to apollo and react native. I'm working on a project where I need to use graphql subscriptions and use graphql queries, which need authentication. From the apollo docs I had set-up my client.ts like:
import { ApolloClient, split, HttpLink, InMemoryCache } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
const wslink = new GraphQLWsLink(
createClient({
url: "ws://localhost:4000/subscriptions",
}),
);
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wslink,
httpLink,
);
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
});
but I need to add the authentication. The documents only mention authentication with this type of setup for the subscription aspect of the code:
const wslink = new GraphQLWsLink(
createClient({
url: "ws://localhost:4000/subscriptions",
connectionParams: {
authToken: user.authToken,
},
}),
);
which doesn't help me and gives me an error because user is unknown. The apollo docs give:
import ApolloClient from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { ApolloLink, concat, split } from "apollo-link";
import { InMemoryCache } from "apollo-cache-inmemory";
import { getMainDefinition } from "apollo-utilities";
const httpLink = new HttpLink({ uri: process.env.VUE_APP_API_TARGET });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem('token');
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : "",
},
});
return forward(operation);
});
export const apolloClient = new ApolloClient({
link: concat(authMiddleware, httpLink),
cache: new InMemoryCache(),
});
as an example of how to add in the authentication aspect. What I'm having trouble with is how do you combine the two? Currently, I have:
import { ApolloClient, split, HttpLink, InMemoryCache } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { ApolloLink, concat} from "apollo-link";
import AsyncStorage from '#react-native-async-storage/async-storage';
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
const wslink = new GraphQLWsLink(
createClient({
url: "ws://localhost:4000/subscriptions",
}),
);
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
const authLink = new ApolloLink((operation, forward) => {
const token = /*await*/ AsyncStorage.getItem('token');
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : "",
},
});
return forward(operation);
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wslink,
httpLink,
);
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
});
In export const client = new ApolloClient({cache: new InMemoryCache(), link: splitLink, });, I tried changing it to link: concat(authLink, splitLink), but that gives errors
"message": "Type 'ApolloLink' is missing the following properties from type 'ApolloLink': onError, setOnError",
and
"message": "Argument of type 'ApolloLink' is not assignable to parameter of type 'ApolloLink | RequestHandler'.\n Type ... ApolloLink'.\n Types of property 'split' are incompatible.\n
I also tried replacing httpLink in const splitLink... with , but that gave the errors:
"message": "Argument of type 'ApolloLink' is not assignable to parameter of type 'ApolloLink | RequestHandler | undefined'.\n Type 'ApolloLink' is missing the following properties from type 'ApolloLink': onError, setOnError",
and
"message": "Argument of type 'HttpLink' is not assignable to parameter of type 'ApolloLink | RequestHandler'.\n Type 'HttpLink' is not assignable to type 'ApolloLink'.\n Types of property 'split' are incompatible.\n...
Does anyone know how to do this? I would really appreciate any help or advice. Thank you!

Apollo GraphQL: GraphQLWsLink (Subscriptions) Troubles. Cannot get WebSocket implementation to work w/ Next.js

So I have a GraphQL server that I wrote in Go, following this tutorial pretty closely. I have my front-end written as a Next.js application, and I am currently trying to create a client to connect to my server and even following the subscription docs to the T, I cannot seem to get it to work. How is it that the examples provided do not include a webSocketImpl?
If I don't provide a webSocketImpl, I get this:
Error: WebSocket implementation missing; on Node you can `import WebSocket from 'ws';` and pass `webSocketImpl: WebSocket` to `createClient`
So, naturally, I import { WebSocket } from "ws"; , and have:
const wsLink = new GraphQLWsLink(
createClient({
webSocketImpl: WebSocket,
url: "ws://localhost:8080/subscriptions",
})
);
Where I then get:
error - ./node_modules/node-gyp-build/index.js:1:0
Module not found: Can't resolve 'fs'
Here is the full code, basically all I need is to create a ApolloClient and export it for use in my React code.
import { ApolloClient, HttpLink, InMemoryCache, split } from "#apollo/client";
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { getMainDefinition } from "#apollo/client/utilities";
import { WebSocket } from "ws";
const wsLink = new GraphQLWsLink(
createClient({
webSocketImpl: WebSocket,
url: "ws://localhost:8080/subscriptions",
})
);
const httpLink = new HttpLink({
uri: `http://localhost:8080/query`,
});
const link = split(
({ query }) => {
const def = getMainDefinition(query);
return (
def.kind === "OperationDefinition" && def.operation === "subscription"
);
},
wsLink,
httpLink
);
export const Client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
Am I totally missing something here? Is there not a default WebSocket implementation in my installation? Obviously the "ws" implementation isn't cutting it, probably because fs is not available in-browser?
A major thing I left off here: I was using Next.js. The reason this was occurring was due to SSR. Basically, we need to only generate the WebSocket link if we are in the browser by using `typeof window !== 'undefined'. This is my updated code:
import { ApolloClient, HttpLink, InMemoryCache, split } from "#apollo/client";
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { getMainDefinition } from "#apollo/client/utilities";
const wsLink =
typeof window !== "undefined"
? new GraphQLWsLink(
createClient({
url: "ws://localhost:8080/subscriptions",
})
)
: null;
const httpLink = new HttpLink({
uri: `http://localhost:8080/query`,
});
const link =
typeof window !== "undefined" && wsLink != null
? split(
({ query }) => {
const def = getMainDefinition(query);
return (
def.kind === "OperationDefinition" &&
def.operation === "subscription"
);
},
wsLink,
httpLink
)
: httpLink;
export const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
I found a way how it`s work with GraphQL-yoga
It's client :
// import { createServer } from "#graphql-yoga/node";
import { makeExecutableSchema } from "#graphql-tools/schema";
import typeDefs from "#/server/graphql/typeDef/schema.graphql";
import resolvers from "#/server/graphql/resolvers";
import dbInit from "#/lib/dbInit";
import JWT from "jsonwebtoken";
import Cors from "micro-cors";
// const pubsub = new PubSub();
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
import {
createServer,
createPubSub,
GraphQLYogaError,
} from "#graphql-yoga/node";
import { useResponseCache } from "#envelop/response-cache";
import { WebSocketServer } from "ws"; // yarn add ws
// import ws from 'ws'; yarn add ws#7
// const WebSocketServer = ws.Server;
import { useServer } from "graphql-ws/lib/use/ws";
const pubSub = createPubSub();
const server = createServer({
cors: {
credentials: "same-origin",
origin: ["http://localhost:3000"], // your frontend url.
},
plugins: [
useResponseCache({
includeExtensionMetadata: true,
}),
],
context: async (ctx) => {
let wsServer = null;
wsServer = ctx.res.socket.server.ws ||= new WebSocketServer({
port: 4000,
path: "/api/graphql",
});
wsServer &&= useServer({ schema }, wsServer);
const db = await dbInit();
let { token, customerId, customerExpire } = ctx.req.cookies;
// 1. Find optional visitor id
let id = null;
if (token) {
try {
let obj = JWT.verify(token, "MY_SECRET");
id = obj.id;
} catch (err) {
console.error("error on apollo server", err); // expired token, invalid token
// TODO try apollo-link-error on the client
throw new AuthenticationError(
"Authentication token is invalid, please log in"
);
}
}
return {
...ctx,
userId: id,
customerId,
pubSub,
};
},
schema,
});
export default server;
And client
import { useMemo } from "react";
import {
ApolloClient,
InMemoryCache,
split,
HttpLink,
createHttpLink,
} from "#apollo/client";
import merge from "deepmerge";
import { getMainDefinition } from "apollo-utilities";
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
// const link = process.env.SERVER_LINK;
let apolloClient;
//create websocket link
const wsLink =
typeof window !== "undefined"
? new GraphQLWsLink(
createClient({
url: "ws://localhost:4000/api/graphql",
on: {
connected: () => console.log("connected client"),
closed: () => console.log("closed"),
},
})
)
: null;
//create http link
const httplink = new HttpLink({
uri: "http://localhost:3000/api/graphql",
credentials: "same-origin",
});
//Split the link based on graphql operation
const link =
typeof window !== "undefined"
? split(
//only create the split in the browser
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscription";
},
wsLink,
httplink
)
: httplink;
//create apollo client
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === "undefined",
link: link,
cache: new InMemoryCache(),
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = merge(initialState, existingCache);
// Restore the cache with the merged data
_apolloClient.cache.restore(data);
}
if (typeof window === "undefined") return _apolloClient;
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}

Could not find "client" in the context or passed in as an option issue when using subscriptions

I need to fetch some data in real-time. So I decided to use the WebSocket connection
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloLink, split } from "apollo-link";
import { WebSocketLink } from "apollo-link-ws";
import { HttpLink } from "apollo-link-http";
import { setContext } from "apollo-link-context";
import { firebaseAppAuth } from "../../App";
import { onError } from "apollo-link-error";
import { getMainDefinition } from "apollo-utilities";
import config from "../../config";
const authLink = setContext((_, { headers }) => {
//it will always get unexpired version of the token
if (firebaseAppAuth && firebaseAppAuth.currentUser) {
return firebaseAppAuth.currentUser.getIdToken().then((token) => {
return {
headers: {
...headers,
"content-type": "application/json",
authorization: token ? `Bearer ${token}` : "",
},
};
});
} else {
return {
headers: {
...headers,
},
};
}
});
const errLink = onError(({ graphQLErrors, networkError }) => {
console.log(graphQLErrors);
console.log(networkError);
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) => {
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
});
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
const httpLink = new HttpLink({
uri: config.adminAPI,
});
const wsLink = new WebSocketLink({
uri: config.apiSocket, // use wss for a secure endpoint
options: {
reconnect: true,
},
});
const splittedLink = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscription";
},
wsLink,
httpLink
);
const link = ApolloLink.from([errLink, authLink, splittedLink]);
const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});
export default client;
I created authLink, errLink, httpLink and wsLink like the above code. and used the split function to combine httpLink and wsLink, it can run queries without any issues, but when I try to run a subscription hook, it throws following error message.
Could not find "client" in the context or passed in as an option. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via options.
I am also passing the client as a prop
ReactDOM.render(
<BrowserRouter>
<ApolloProvider client={client}>
<SnackbarProvider maxSnack={5}>
<SnackbarUtilsConfigurator />
<Route path="/" component={App} />
</SnackbarProvider>
</ApolloProvider>
</BrowserRouter>,
document.getElementById("root")
);
How to resolve this issue?
Could you please share how are you using ApolloProvider? It looks like you are missing this:
import React from 'react';
import { render } from 'react-dom';
import { ApolloProvider } from '#apollo/client/react';
const client = new ApolloClient({ uri, cache });
function App() {
return (
<div>
<h2>My first Apollo app 🚀</h2>
</div>
);
}
render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root'),
);
Make sure that you're passing client as a prop to ApolloProvider

Apollo Subscription with Gatsby Setup

below is my setup of Gatsby SSR with Apollo for those looking for a solution on how to set them up. Hope it helps those looking online for a solution on how to set up Gatsby with Apollo
Credits: [https://github.com/apollographql/subscriptions-transport-ws/issues/333]
I am not sure at the moment if my onError variable is placed correctly. I will look it up
Nonetheless, subscription with gatsby client-side routes is working fine with this setup
client.js // to be imported into ApolloProvider
import ApolloClient from 'apollo-client'
// import * as ws from 'ws'
// import { createHttpLink } from 'apollo-link-http'
import { split } from 'apollo-link'
// import { setContext } from 'apollo-link-context'
import { onError } from 'apollo-link-error'
import { HttpLink } from 'apollo-link-http'
import { WebSocketLink } from 'apollo-link-ws'
// import fetch from 'isomorphic-fetch' // Comment out this line results in an error ...
import 'isomorphic-fetch'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { getMainDefinition } from 'apollo-utilities'
const HTTP_URI = `http://localhost:4000/graphql`
const WS_URI = `ws://localhost:4000/graphql`
const httplink = new HttpLink({
uri: HTTP_URI,
// credentials: 'same-origin',
// fetch,
})
const wsLink = process.browser
? new WebSocketLink({
// if you instantiate in the server, the error will be thrown
uri: WS_URI,
options: {
reconnect: true,
},
})
: null
const errorLink = onError(({ networkError, graphQLErrors }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
)
}
if (networkError) console.log(`[Network error]: ${networkError}`)
})
const link = process.browser
? split(
//only create the split in the browser
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
httplink,
errorLink
)
: httplink
export const client = new ApolloClient({
link,
cache: new InMemoryCache(),
})
graphql/subscriptions.js
import gql from 'graphql-tag'
const BOOK_ADDED_SUBSCRIPTION = gql`
subscription {
bookAdded {
_id
title
author
}
}
`
export { BOOK_ADDED_SUBSCRIPTION }

How to do graphql and graphql subscriptions with svelte

To do graphql queries and mutations ive had success with both fetch and svelte-apollo (see https://github.com/timhall/svelte-apollo)
I like the fech approach for its simplicity.
Svelte-apollo features subscriptions and I will try to get it to work.
But are there alternatives?
How do you consume graphql subscriptions with svelte?
Heres my solution, using Apollo:
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
});
const wsLink = new WebSocketLink({
uri: `ws://localhost:3000/subscriptions`,
options: {
reconnect: true
}
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link,
cache: new InMemoryCache()
});
After all that, you have your client set up. Next,
import gql from 'graphql-tag';
client.subscribe({
query: gql`subscription { whatever }`
}).subscribe(result => console.log(result.data);
I'm using urql's svelte bindings. The documentation also shows how to use the bindings with subscriptions.

Resources