How do I globally handle my query and mutation errors? When I use a malformed graphql query, in a mutation or query, my react-native app always throws the following error.
Unhandled (in react-apollo:Apollo(MyScreen)) Error: Network error:
Response not successful: Received status code 400
Running the same queries on the graphiql endpoint, provide relevant errors however. While the goal is globally handled errors, local query errors also do not work for me. Printing them shows nothing.
//MyComponent.js
class MyComponent extends React.Component {
render () {
/*
outputs
{
error: undefined
loading: true
networkStatus: 1
}
*/
console.log(this.props.data);
//outputs undefined
console.log('error', this.props.error);
//outputs undefined
console.log('errors', this.props.errors);
return (
<Container>
{this.props.myData}
<Button block onPress={this.props.onPress}>
<Text>Button</Text>
</Button>
</Container>
)
}
}
const mapDispatchToProps = (dispatch, {navigation: {navigate}}) => ({
onPress: async rest => {
//do stuff
}
});
export default connect(null, mapDispatchToProps)(MyDataService.getMyDataInjector(MyComponent));
//MyDataService.js
import { graphql } from 'react-apollo';
import getApolloClient from '../../../apolloClient';
import { dataFragment } from './dataFragments';
import gql from 'graphql-tag';
export default MyDataService = {
getMyDataInjector: graphql(gql`
query {
myData {
...dataFragment
}
}
${dataFragment}
`,
{
props: ({ data, errors, error }) => ({
data,
loading: data.loading,
myData: data.myData,
errors,
error,
})
}),
addData: async (data) => {
const res = await apolloClient.mutate({
mutation: gql`
mutation addData($data: String!) {
addData(data: $data) {
...dataFragment
}
}
${dataFragment}
`,
variables: {
data,
}
});
return res.data.addData;
},
};
//apolloClient.js
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
let apolloClient;
const initApolloClient = store => {
const httpLink = new HttpLink({uri: 'http://192.168.0.11:3000/graphql'})
// const errorLink = onError(({ response, graphQLErrors, networkError }) => {
// console.log('graphql error in link', graphQLErrors);
// console.log('networkError error in link', networkError);
// console.log('response error in link', response);
// 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 authLink = setContext((_, { headers }) => {
const {accessToken: {type, token}} = store.getState().signedInUser;
return {
headers: {
...headers,
authorization: type && token ? type + ' ' + token : null
}
}
});
apolloClient = new ApolloClient({
// using error link throws - Error: Network error: forward is not a function
// link: authLink.concat(errorLink, httpLink),
link: authLink.concat(httpLink),
// doesn't console log
// onError: (e) => { console.log('IN ON ERROR', e.graphQLErrors) },
cache: new InMemoryCache({
dataIdFromObject: o => (o._id ? `${o.__typename}:${o._id}`: null),
}),
// turning these on do nothing
// defaultOptions: {
// query: {
// // fetchPolicy: 'network-only',
// errorPolicy: 'all',
// },
// mutate: {
// errorPolicy: 'all'
// }
// }
});
return apolloClient;
}
export default getApolloClient = () => apolloClient;
export { initApolloClient };
was able to get it working by using apollo-link with onError.
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
let apolloClient;
const initApolloClient = store => {
const httpLink = new HttpLink({uri: 'http://192.168.0.11:3000/graphql'})
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.error(
`[GraphQL error]: Message: ${message}, Location: ${JSON.stringify(locations)}, Path: ${path}`,
),
);
}
if (networkError) console.error(`[Network error]: ${networkError}`);
});
const authLink = setContext((_, { headers }) => {
const {accessToken: {type, token}} = store.getState().signedInUser;
return {
headers: {
...headers,
authorization: type && token ? type + ' ' + token : null
}
}
});
const link = ApolloLink.from([
authLink,
errorLink,
httpLink,
]);
apolloClient = new ApolloClient({
link,
cache: new InMemoryCache({
//why? see https://stackoverflow.com/questions/48840223/apollo-duplicates-first-result-to-every-node-in-array-of-edges/49249163#49249163
dataIdFromObject: o => (o._id ? `${o.__typename}:${o._id}`: null),
}),
});
return apolloClient;
}
Related
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, 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}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
I'm trying to handle errors on my React Native with Expo app, while using Apollo and Graphql.
The problem is that in my apolloServices.js I'm trying to use onError function, and despite I tried with both apollo-link-error and #apollo/client/link/error the import is still greyed out. The function it's at the ApolloClient.
React-native: 0.63.2
#Apollo-client: 3.3.11
Apollo-link-error: 1.1.13
// Apollo resources
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from '#apollo/client'; // Tried putting onError here
import { onError } from 'apollo-link-error'; // And here
//import { onError } from '#apollo/client/link/error'; // Also here
import {
InMemoryCache,
defaultDataIdFromObject,
} from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
/* Constants */
import { KEY_TOKEN } from '../constants/constants';
/**
* #name ApolloServices
* #description
* The Apollo Client's instance. App.js uses it to connect to graphql
* #constant httpLink HttpLink Object. The url to connect the graphql
* #constant defaultOPtions Object. Default options for connection
* #constant authLink
*/
const httpLink = new HttpLink({
uri: 'workingUri.com',
});
const defaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
};
//Create link with auth header
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
return AsyncStorage?.getItem(KEY_TOKEN).then((token) => {
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
'auth-token': token ? token : '',
},
};
});
});
export default new ApolloClient({
/* ON ERROR FUNCTION HERE*/
onError: (graphQLErrors, networkError) => {
if (graphQLErrors)
graphQLErrors.map(
({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
},
cache: new InMemoryCache(),
defaultOptions: defaultOptions,
link: authLink.concat(httpLink),
request: (operation) => {
operation.setContext({
headers: {
'auth-token': token ? token : '',
},
});
},
});
Appart from this, the app is working perfectly, what I want it's just to handle graphql and network errors
The best solution would be to use this approach
const errorControl = 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)
};
});
export default new ApolloClient({
cache: new InMemoryCache(),
defaultOptions: defaultOptions,
link: errorControl.concat(authLink.concat(httpLink)),
headers: {
authorization: token ? token : '',
},
});
Can successfully register the user using my action creator but it returns undefined. I think it's the way how am returning my dispatch
axiosInstance
import axios from 'axios';
import AsyncStorage from '#react-native-community/async-storage';
// import base url
import {API_URL} from '../constants';
const instance = axios.create({
baseURL: API_URL,
timeout: 2000,
});
instance.interceptors.request.use(
async(config) => {
const token = await AsyncStorage.getItem('token');
if(token) {
config.headers.Autherization = `${token}`;
}
return config;
},`enter code here`
(err) => {
return Promise.reject(err);
}
)
export default instance;
SignUP Action.
import axiosInstance from '../../api/axiosInstance';
import {REGISTER_USER_SUCCESS, REGISTER_USER_FAIL} from '../actionTypes/index';
const registerSuccess = (payload) => {
return{
type: REGISTER_USER_SUCCESS,
data: payload
}
};
const registerError = (payload) => {
return {
type: REGISTER_USER_FAIL,
data: payload
}
};
export const SignUp = (registerData) => async dispatch => {
axiosInstance.post('/users/register', registerData)
.then((response)=> {
dispatch(registerSuccess(response.data));
})
.catch((error) => {
dispatch(registerError(error));
});
}
Here is how am using my action creator .. the result is undefined . I want to have a check some that I can redirect the screen to another login screen or home screen
SignUP Submit function
dispatch(registerAction.SignUp(values))
.then( (result) => {
console.log('klhadsghaj',result.status);
if(result.success) {
try {
navData.navigation.navigate("Login");
}catch (err) {
console.log(err)
}
} else {
Alert.alert('Registration failed. Try Again')
}
})
.catch(err => console.log(err))
So according to the apollo docs for apollo-link-error, onError can be used to handle re-authentication if used with forward(operation).
So I wrote the following code
import { ApolloClient } from 'apollo-client'
import { createHttpLink, HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { InMemoryCache } from 'apollo-cache-inmemory'
import AsyncStorage from '#react-native-community/async-storage'
import { refresh } from 'react-native-app-auth'
import { onError } from 'apollo-link-error'
import { ApolloLink, from } from 'apollo-link'
import { RetryLink } from "apollo-link-retry"
import { KC_CONFIG } from '../../config/env'
const httpLink = new HttpLink({
uri: 'graphqlEndpointOfYourchoice'
})
const authLink = setContext(async (_, { headers }) => {
const accessToken = await AsyncStorage.getItem('accessToken')
const unwrappedAccessToken = JSON.parse(accessToken)
return {
headers: {
...headers,
authorization: unwrappedAccessToken ? `Bearer ${unwrappedAccessToken}` : "",
}
}
})
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
AsyncStorage.getItem('refreshToken')
.then(data => {
const refreshToken = JSON.parse(data)
// console.log(data)
refresh(KC_CONFIG, {
refreshToken,
})
.then(({ accessToken, refreshToken }) => {
const oldHeaders = operation.getContext().headers
operation.setContext({
...oldHeaders,
authorization: accessToken
})
console.log(oldHeaders.authorization)
console.log(accessToken)
// console.log(refreshToken)
AsyncStorage
.multiSet([
['accessToken', JSON.stringify(accessToken)],
['refreshToken', JSON.stringify(refreshToken)]
])
// tried putting forward() here <--------------
})
.catch(e => {
if (e.message === 'Token is not active') console.log('logging out')
else console.log('Refresh error: ' + e)
})
})
.then(() => {
console.log('Refreshed the accesstoken')
return forward(operation)
})
.catch(e => {
console.log('Storage error: ' + e)
})
}
if (networkError) {
console.log('network error: ' + networkError)
}
// tried putting forward() here <--------------
})
const retryLink = new RetryLink()
export const client = new ApolloClient({
link: from([
retryLink,
errorLink,
authLink,
httpLink
]),
cache: new InMemoryCache()
})
This does not achieve the desired result.
The error gets caught and runs it's course, refreshing the token as it should, but it never does a second request.
try this:
export const logoutLink = onError(({ networkError, operation, forward }) => {
if (networkError?.statusCode === 401) {
return new Observable(observer => {
(async () => {
try {
const newToken = await getToken();
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${newToken}`,
},
});
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
};
// Retry last failed request
forward(operation).subscribe(subscriber);
} catch (error) {
observer.error(error);
}
})();
});
}
});
I have an app authenticating fine and returning the access_token and refresh_token. I store them with AsyncStorage and save/get the access_token with redux. This is the very first app I am building and I am struggling with how and where to use the refresh_token.
This is the axios call in the component loginForm.js
axios({
url: `${base}/oauth/token`,
method: 'POST',
data: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
}
})
.then(response => {
setStatus({ succeeded: true });
// console.log(response.data);
deviceStorage.saveKey("userToken", response.data.access_token);
deviceStorage.saveKey("refreshToken", response.data.refresh_token);
Actions.main();
})
.catch(error => {
if (error.response) {
console.log(error);
}
});
This is the service deviceStorage.js
import { AsyncStorage } from 'react-native';
const deviceStorage = {
async saveItem(key, value) {
try {
await AsyncStorage.setItem(key, value);
} catch (error) {
console.log('AsyncStorage Error: ' + error.message);
}
}
};
export default deviceStorage;
This is the token action file
import { AsyncStorage } from 'react-native';
import {
GET_TOKEN,
SAVE_TOKEN,
REMOVE_TOKEN,
LOADING_TOKEN,
ERROR_TOKEN
} from '../types';
export const getToken = token => ({
type: GET_TOKEN,
token,
});
export const saveToken = token => ({
type: SAVE_TOKEN,
token
});
export const removeToken = () => ({
type: REMOVE_TOKEN,
});
export const loading = bool => ({
type: LOADING_TOKEN,
isLoading: bool,
});
export const error = tokenError => ({
type: ERROR_TOKEN,
tokenError,
});
export const getUserToken = () => dispatch =>
AsyncStorage.getItem('userToken')
.then((data) => {
dispatch(loading(false));
dispatch(getToken(data));
})
.catch((err) => {
dispatch(loading(false));
dispatch(error(err.message || 'ERROR'));
});
export const saveUserToken = (data) => dispatch =>
AsyncStorage.setItem('userToken', data)
.then(() => {
dispatch(loading(false));
dispatch(saveToken('token saved'));
})
.catch((err) => {
dispatch(loading(false));
dispatch(error(err.message || 'ERROR'));
});
export const removeUserToken = () => dispatch =>
AsyncStorage.removeItem('userToken')
.then((data) => {
dispatch(loading(false));
dispatch(removeToken(data));
})
.catch((err) => {
dispatch(loading(false));
dispatch(error(err.message || 'ERROR'));
});
This is the token reducer file
import {
GET_TOKEN,
SAVE_TOKEN,
REMOVE_TOKEN,
LOADING_TOKEN,
ERROR_TOKEN
} from '../actions/types';
const INITIAL_STATE = {
token: {},
loading: true,
error: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case GET_TOKEN:
return {
...state,
token: action.token
};
case SAVE_TOKEN:
return {
...state,
token: action.token
};
case REMOVE_TOKEN:
return {
...state,
token: action.token
};
case LOADING_TOKEN:
return {
...state,
loading: action.isLoading
};
case ERROR_TOKEN:
return {
...state,
error: action.error
};
default:
return state;
}
};
And this is the authentication file
import React from 'react';
import {
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { connect } from 'react-redux';
import { Actions } from 'react-native-router-flux';
import { Spinner } from '../common';
import { getUserToken } from '../../actions';
class AuthLoadingScreen extends React.Component {
componentDidMount() {
this.bootstrapAsync();
}
bootstrapAsync = () => {
this.props.getUserToken().then(() => {
if (this.props.token.token !== null) {
Actions.main();
} else {
Actions.auth();
}
})
.catch(error => {
this.setState({ error });
});
};
render() {
return (
<View style={styles.container}>
<Spinner />
<StatusBar barStyle="default" />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
});
const mapStateToProps = state => ({
token: state.token,
});
const mapDispatchToProps = dispatch => ({
getUserToken: () => dispatch(getUserToken()),
});
export default connect(mapStateToProps, mapDispatchToProps)(AuthLoadingScreen);
I believe I need to create an action and reducer to get the refresh_token (is that correct?) but I do not know what to do with it and where to call it (perhaps in the authentication file?).
Any help with this possibly with code examples related to my code would be massively appreciated. Thanks
Below are the steps
Do Login , get accessToken , refreshToken from response and save it to AsyncStorage.
Make common function for API calling
async function makeRequest(method, url, params, type) {
const token = await AsyncStorage.getItem('access_token');
let options = {
method: method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
},
};
if (!token) {
delete options['Authorization'];
}
if (['GET', 'OPTIONS'].includes(method)) {
url += (url.indexOf('?') === -1 ? '?' : '&') + queryParams(params);
} else {
Object.assign(options, {body: JSON.stringify(params)});
}
const response = fetch(ENV.API_URL+url, options);
return response;
}
Make one method in redux for getAceessTokenFromRefreshToken.
Use this method when session is expired
How do you know session is expired?
From each API calling if you get response like (440 response code) in
async componentWillReceiveProps(nextProps) {
if (nextProps.followResponse && nextProps.followResponse != this.props.followResponse) {
if (nextProps.followResponse.status) {
if (nextProps.followResponse.status == 440) {
// call here get acceesstokenfrom refresh token method and save again accesstoken in asyncstorage and continue calling to API
}
}
}
}