Apollo-link: networkError at apollo-link-error doesn't have status attribute

Created on 2 Dec 2017  ·  35Comments  ·  Source: apollographql/apollo-link

As subject. Thanks

const errorLink = onError(({ networkError }) => {
  if (networkError.status === 401) {
    logout();
  }
})

screen shot 2017-12-03 at 4 49 20 am

Most helpful comment

So for anyone still looking...

The statusCode value is returned as a property on networkError, and the updated typing for networkError was added here: https://github.com/apollographql/apollo-link/pull/530

Since networkError is now a union type of Error | ServerError | ServerParseError, Typescript will only allow us to access properties that are common to all types in the union. statusCode does not exist on type Error. We won't be able to access it via networkError.statusCode without Typescript complaining; we need to use a type guard or something else to differentiate the types.

Several methods are suggested in the Typescript handbook: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

For my purposes, I used the in operator to get things working.

  onError: ({ networkError }: ErrorResponse) => {
    if (
      networkError &&
      'statusCode' in networkError &&
      networkError.statusCode === 401
    ) {
      // perform logout stuff
    }
  },

@JoviDeCroock Think this issue is safe to close?

All 35 comments

As I know "failed to fetch" happens if there is no response (e.g. server is down). That because you will not see any status code. Please correct me if I am wrong.

But I think in such situations it's not the best idea to only have a text message - there should be something that can be handled more easily (like an error code).

i did see request was sending out and in (401) in the network tag of chrome inspector.

I think this is related to #218 (not sure..)

Also having this issue. networkError.status or networkError.statusCode are missing and response object is missing. There's no way to handle HTTP status codes in network layer.

I've also got this issue currently. It was supposedly fixed in the latest release, but I'm still having this issue.

any thoughts on this @jbaxleyiii ?

I'm sorry to hear that you @akrigline and @bogdansoare are still having issues with this! This should definitely be fixed.

I'm working through #364 right now. In the meantime, could you create or modify this code sandbos to reproduce the error? Or even better open a PR with a failing test?

any news?

Also having this issue, the response object is empty and there’s no statusCode on the networkError object.

I don’t even get the networkError property:
screen shot 2018-05-07 at 13 01 56

EDIT: Most likely #475

I've fixed this issue by ignoring the Error type, But this is a completely temporary solution and we have to wait for future fixes. @evans

Here is this problem!

The current of the networkError that we wrote about them was not a real server-side error. In summary, we will not have statusCode at all in these cases and in fact, We need to create or receive a real network error on the graphql server. (_If you have just a communication issue with the server, you can't access to the statusCode_).

screen shot 2018-05-07 at 20 38 09

Typeface error is still remaining!

If you are using TypeScript, you can see that networkError has the following type:

export interface ErrorResponse {
  graphQLErrors?: GraphQLError[];
  networkError?: Error;
  response?: ExecutionResult;
  operation: Operation;
}

And Error is defined as:

interface Error {
    name: string;
    message: string;
    stack?: string;
}

We can see that the statusCode is not defined here, So we can't call something like networkError.statusCode in TypeScript.

My temporary solution

We have to cast networkError as any and also we make it as a empty object.

const afterWareLink = onError(({operation, response, graphQLErrors = {}, networkError = {} as any}) => {
    const status: number = networkError && networkError.statusCode ? networkError.statusCode : null;
    debugHelper.error('apolloError', {
        operation,
        response,
        graphQLErrors,
        networkError,
        status,
    });

    // Do your job
    // if (status && HTTPExceptions[status])
    //     redirectService.redirectWithReload(`/error/${status}`);
});

According to the above afterWareLink codes, we encountered server error with 400 status code and now will be able to access this status code:

screen shot 2018-05-07 at 20 28 03

Here is my ApolloClient definition:

import {ApolloLink, from} from 'apollo-link';
import {application} from '../../constants/application';
import {ApolloClient} from 'apollo-client';
import {InMemoryCache} from 'apollo-cache-inmemory';
import {HttpLink} from 'apollo-link-http';
import {tokenStorage} from '../middleware';
import {debugHelper} from '../helpers/debugHelper';
import {onError} from 'apollo-link-error';
import {apolloCache} from './apolloCache';
import fetch from 'unfetch';

const API = new HttpLink({
    uri: application.API.URL,
    fetch: fetch
});

const cache = new InMemoryCache({
    dataIdFromObject: (object: any) => apolloCache.getID(object)
});

const afterWareLink = onError(({operation, response, graphQLErrors = {}, networkError = {} as any}) => {
    const status: number = networkError && networkError.statusCode ? networkError.statusCode : null;
    debugHelper.error('apolloError', {
        operation,
        response,
        graphQLErrors,
        networkError,
        status,
    });

    // Do your job
    // if (status && HTTPExceptions[status])
    //     redirectService.redirectWithReload(`/error/${status}`);
});

const middleWareLink = new ApolloLink((operation, forward) => {
    const token = tokenStorage.get();
    operation.setContext(context => ({
        ...context,
        headers: {
            ...context.headers,
            token: token ? token.token : '',
        },
    }));

    return forward(operation);
});

export const apolloClient = new ApolloClient({
    link: from([
        middleWareLink,
        afterWareLink,
        API
    ]),
    cache: cache,
});

Hope to be useful.

If you're using a node server and have access to your response object from the context you can solve this by adding; ctx.res.status(401); before returning your response.
Now apollo-link-errors and apollo-link-retry will both be able to pick up the error from the networkErrors object.

Here's a example where I'm using a custom schema directive to make sure a user is authenticated;

import { SchemaDirectiveVisitor } from "graphql-tools";
import { defaultFieldResolver } from "graphql";
import { createError } from "apollo-errors";

const AuthError = createError("AuthError", {
  message: "Not authorized"
});

export class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
  visitObject(type) {
    this.ensureFieldsWrapped(type);
  }
  visitFieldDefinition(field, details) {
    this.ensureFieldsWrapped(details.objectType);
  }

  ensureFieldsWrapped(objectType) {
    if (objectType._authFieldsWrapped) return;
    objectType._authFieldsWrapped = true;

    const fields = objectType.getFields();
    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      const { resolve = defaultFieldResolver } = field;
      field.resolve = async function(...args) {
        const ctx = args[2];
        const result = await resolve.apply(this, args);
        if (ctx.req.user) {
          return result;
        } else {
          ctx.res.status(401);
          return null;
        }
      };
    });
  }
}

Was it fixed or not? I still can not access any status code in networkError object.

@artsiompeshko
I can access it this way in TypeScript:

const networkErrorLink = onError(({networkError, operation, forward}: ErrorResponse) => {
  if (networkError) {
    switch (networkError['statusCode']) {
      case 401:
      case 422:
        if (window.location.pathname !== '/login') {
          Logger.error('Unauthorized or stale Authorization Session. Reloading page...')
          window.history.go()
        }
        break
    }
  }
  return forward(operation)
})

Any news on this? Same problem. No networkError.statusCode available in my javascript code.

@goldenbearkin maybe are we wrong using it correctly?

We should allow network status code in the error, currently there is no way to access network status. It the networkError is actually a HttpErrorResponse type object which has a status property which is always 0 where the original status code in the console is 401 or 403

@lvlohammadi
Can the same workaround be done for graphQLErrors as well? I can see the status code for networkErrors using your workaround - tested this with a 504. But when authentication fails, I expect a graphQLError status code of 401. But no way to access it currently. I tried the 'graphQLErrors = {} as any' option, but to no avail.

This is weird, but I actually have statusCode property on networkError. However, there is no such property in the interface, so TS outputs error here.

const errorLink = onError(({ networkError, graphQLErrors, response }: ErrorResponse) => {
  if (graphQLErrors) {
    graphQLErrors.map(({ message, locations, path }: GraphQLError) => {
      showError(message);
    });
  }

  // @ts-ignore
  console.log('statusCode', networkError.statusCode); // outputs 401

  if (networkError) {
    showError(networkError.message);
  }
});

On the server I use koa and koa-passport, and corresponding piece of code here is:

app.use(mount(graphQlPath, passport.authenticate('jwt', { session: false })));

Since this issue is really outdated I am closing it but if you are still concerned about this feel free to reopen and I'll get back to you asap.

@JoviDeCroock facing that issue right now... so, what is the solution? How do I get error status from networkError?

Allright, so you are sure that it's a networkError (no graphqlError?) and statusCode/status is undefined?

Is it a typescript error or really undefined? Can you reproduce it?

Btw downvoting my comment does not help resolving this error, there was never any way for this issue to be reproduced plus the issues needed to be trimmed to improve the way we can help out.

I have the same issue, however it could be because of how I'm sending the status from the server. I'm basically trying to shortcut any of the graphQL specific handling and simply responding with, for example:
res.status(404).send("The resource you are looking for cannot be found")

However I only get TypeError: Failed To Fetch. Is this because of implementation details in Apollo-Server-Express?

@JoviDeCroock Actually, I messed up a little bit with graphql code when trying to implement client side error handling for 401 or 403. We use middleware, and what I did is just giving back always 401 for every request. That way also OPTION request got 401, what should not happens, and probably because of that I was not able to see status at all. After updating the code, I am able to see status codes

Thank for the update @dimitriy-k happy to hear that this issue is resolved.

For @AlbertoPL, I'll look into your issue tonight. Feel free to give more comments about it if you can.

@JoviDeCroock Thanks for taking a look. So the specific code that I'm trying to get to work is the following:
res.status(426).send()

In Chrome's network tab I do see that the /graphql endpoint is responding with a 426 status code and appropriate response (Upgrade Required). However I still only see TypeError: Failed to Fetch When using the onError link from apollo-link-error. If I could get the true status code that would be great (or even the message would suffice).

Allright, that's some pretty great information. We should be able to debug this somehow, do you feel up to this or?

Yeah I can certainly try. I'd appreciate any help/tips to do so.

So just stepping through code in Chrome's debugger, I can see that both apollo-link-batch-http and apollo-link-http make API calls using the fetcher function (either passed as a link option or some default, I tried both):

fetcher(chosenURI, options)
    .then(function (response) {
        operations.forEach(function (operation) { return operation.setContext({ response: response }); });
        return response;
    }).then(parseAndCheckHttpResponse(operations))

It never goes into any of the then blocks, but instead going into the catch block. By then the error is already set to "Failed to Fetch". No status code is present.

We should check if it goes wrong here:

https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L118

I'll try to make a reproduction this weekend, having a pretty busy schedule atm at work.

EDIT:

The main problem is everytime I test this I am using apollo-server and it goes well but it could be that the bindings or something are a bit different. So it's hard for me to test this without a server as reproduction

If it will help, here's our Apollo-server (note we are using Apollo-server-express). CreateGraphQLSchema returns the instance of Apollo Server that we use:

const { ApolloServer, gql } = require("apollo-server-express");

const collectFragments = container => {
  return container
    .$list()
    .map(component => container[component])
    .join("\n");
};

const mergeObjects = container => {
  const types = container.$list().map(k => container[k]);
  return _.merge(...types);
};

function CreateGraphQLSchema(
  schema,
  queries,
  mutations,
  subscriptions,
  resolvers,
  graphqlFormatError,
) {
  const graphQlSchema = gql`
    ${collectFragments(schema.enum)}

    ${collectFragments(schema.type)}

    type Query {
      ${schema.Query}
    }

    ${collectFragments(schema.input)}

    type Mutation {
      ${schema.Mutation}
    }

    type Subscription {
      ${schema.Subscription}
    }

    schema {
      query: Query
      mutation: Mutation
      subscription: Subscription
    }
  `;

  const resolverMap = {};
  resolverMap.Query = mergeObjects(queries);
  resolverMap.Mutation = mergeObjects(mutations);
  resolverMap.Subscription = mergeObjects(subscriptions);
  resolvers.$list().forEach(type => (resolverMap[type] = resolvers[type]));

  return new ApolloServer({
    typeDefs: graphQlSchema,
    resolvers: resolverMap,
    context: ({ req, res }) => {
      return { req, res, user: req.user };
    },
    formatError: graphqlFormatError,
    playground: process.env.NODE_ENV !== "production",
  });
}

@AlbertoPL I ran into this issue too. It turns out its a CORS issue, not Apollo client.

A fetch() promise will reject with a TypeError when a network error is encountered or CORS is misconfigured on the server side

Make sure you are returning the proper headers when rejecting the request. In my case, I was using API Gateway and my custom authorizer was rejecting the request, but not attaching the Access-Control-Allow-Origin header. I use serverless to manage deployments, but it can be converted to CloudFormation templates if that's what's used: https://serverless.com/blog/cors-api-gateway-survival-guide/#cors-with-custom-authorizers.

Nice find @chris-feist!
Adding the Access-Control-Allow-Origin header to the response made the status code available to apollo-client

In express.js, I simply did the following:

res.set('Access-Control-Allow-Origin', '*').status(401).send()

Doesn't using Access-Control-Allow-Origin allow a potential security risk? https://stackoverflow.com/questions/12001269/what-are-the-security-risks-of-setting-access-control-allow-origin

So for anyone still looking...

The statusCode value is returned as a property on networkError, and the updated typing for networkError was added here: https://github.com/apollographql/apollo-link/pull/530

Since networkError is now a union type of Error | ServerError | ServerParseError, Typescript will only allow us to access properties that are common to all types in the union. statusCode does not exist on type Error. We won't be able to access it via networkError.statusCode without Typescript complaining; we need to use a type guard or something else to differentiate the types.

Several methods are suggested in the Typescript handbook: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

For my purposes, I used the in operator to get things working.

  onError: ({ networkError }: ErrorResponse) => {
    if (
      networkError &&
      'statusCode' in networkError &&
      networkError.statusCode === 401
    ) {
      // perform logout stuff
    }
  },

@JoviDeCroock Think this issue is safe to close?

@mrkrlli Thanks. Decent solution.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

j3ddesign picture j3ddesign  ·  3Comments

vjpr picture vjpr  ·  3Comments

ash0080 picture ash0080  ·  4Comments

Nickersoft picture Nickersoft  ·  3Comments

Kisepro picture Kisepro  ·  4Comments