Stretch0
Stretch0

Reputation: 9283

How to set http status code in GraphQL

I want to set an http status code in my GraphQL authentication query, depending on if auth attempt was successful (200), unauthorised (401) or missing parameters (422).

I am using Koa and Apollo and have configured my server like so:

const graphqlKoaMiddleware = graphqlKoa(ctx => {
  return ({
    schema,
    formatError: (err) => ({ message: err.message, status: err.status }),
    context: {
      stationConnector: new StationConnector(),
      passengerTypeConnector: new PassengerTypeConnector(),
      authConnector: new AuthConnector(),
      cookies: ctx.cookies
    }
  })
})

router.post("/graphql", graphqlKoaMiddleware)

As you can see, I have set my formatError to return a message and status but currently only the message is getting returned. The error message comes from the error that I throw in my resolver function.

For example:

const resolvers = {
  Query: {
    me: async (obj, {username, password}, ctx) => {
      try {
        return await ctx.authConnector.getUser(ctx.cookies)  
      }catch(err){
        throw new Error(`Could not get user: ${err}`);
      }
    }
  }
}

My only issue with this method is it is setting the status code in the error message and not actually updating the response object.

Does GraphQL require a 200 response even for failed queries / mutations or can I some how update the response objects status code? If not, How do I set the aforementioned error object status code?

Upvotes: 3

Views: 12018

Answers (7)

Faizan Ali
Faizan Ali

Reputation: 1

The setHttpPlugin you've defined is a custom Apollo Server plugin. Apollo Server provides a plugin system that allows you to extend its functionality by defining custom plugins.

In your setHttpPlugin, you define a plugin that modifies the HTTP response status code based on the presence of errors in the response.

async function StartServer() {
  const setHttpPlugin = {
    async requestDidStart() {
      return {
        async willSendResponse({ response }) {
          response.http.status = response.errors[0].status || 500
        },
      }
    },
  }
  const app = express()

  const PORT = process.env.PORT || 3002
  const Server = new ApolloServer({
    typeDefs,
    resolvers,
    status400ForVariableCoercionErrors: true,
    csrfPrevention: false,
    context: async ({ req }) => {
      return req
    },
    formatError: (err) => {
      console.log(err)
      return {
        message: err.message || 'Interal server error plase try again',
        status: err.extensions.http.status || 500,
        code: err.extensions.code || 'INTERNAL_SERVER_ERROR',
      }
    },
    plugins: [ApolloServerPluginLandingPageGraphQLPlayground, setHttpPlugin],
  })}


export const createPostResolver = async (_, { postInfo }, context) => {
  const { id, error } = await ProtectRoutes(context)
  if (error) {
    throw new GraphQLError('Session has expired', {
      extensions: {
        code: 'BAD_REQUEST',
        http: {
          status: 401,
        },
      },
    })
  }}

Upvotes: 0

tanner burton
tanner burton

Reputation: 1159

apollo-server-express V3 supports this. Create your own plugin. Then you can look in the errors that are thrown to determine the status code.

  import {ApolloServerPlugin} from "apollo-server-plugin-base/src/index";
  
  const statusCodePlugin:ApolloServerPlugin  = {
    async requestDidStart(requestContext) {
      return {
        async willSendResponse(requestContext) {
          const errors = (requestContext?.response?.errors || []) as any[];
          for(let error of errors){
            if(error?.code === 'unauthorized'){
              requestContext.response.http.status = 401;
            }
            if(error?.code === 'access'){
              requestContext.response.http.status = 403;
            }
          }
        }
      }
    },
  };
  
  export default statusCodePlugin;

Upvotes: 0

Vytautas Pranskunas
Vytautas Pranskunas

Reputation: 890

Based on Daniels answer i have managed to write middleware.

import { HttpQueryError, runHttpQuery } from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';


// Source taken from: https://github.com/apollographql/apollo-server/blob/928f70906cb881e85caa2ae0e56d3dac61b20df0/packages/apollo-server-express/src/ApolloServer.ts
// Duplicated apollo-express middleware
export const badRequestToOKMiddleware = (apolloServer: ApolloServer) => {
return async (req, res, next) => {
  runHttpQuery([req, res], {
    method: req.method,
    options: await apolloServer.createGraphQLServerOptions(req, res),
    query: req.method === 'POST' ? req.body : req.query,
    request: req,
  }).then(
    ({ graphqlResponse, responseInit }) => {
      if (responseInit.headers) {
        for (const [name, value] of Object.entries(responseInit.headers)) {
          res.setHeader(name, value);
        }
      }
      res.statusCode = (responseInit as any).status || 200;

      // Using `.send` is a best practice for Express, but we also just use
      // `.end` for compatibility with `connect`.
      if (typeof res.send === 'function') {
        res.send(graphqlResponse);
      } else {
        res.end(graphqlResponse);
      }
    },
    (error: HttpQueryError) => {
      if ('HttpQueryError' !== error.name) {
        return next(error);
      }

      if (error.headers) {
        for (const [name, value] of Object.entries(error.headers)) {
          res.setHeader(name, value);
        }
      }

      res.statusCode = error.message.indexOf('UNAUTHENTICATED') !== -1 ? 200 : error.statusCode;
      if (typeof res.send === 'function') {
        // Using `.send` is a best practice for Express, but we also just use
        // `.end` for compatibility with `connect`.
        res.send(error.message);
      } else {
        res.end(error.message);
      }
    },
  );
};
  }

app.use(apolloServer.graphqlPath, badRequestToOKMiddleware(apolloServer));

Upvotes: 0

Kuramochi-coder
Kuramochi-coder

Reputation: 1

Try adding response and setting the response status code as so, assuming your err.status is already an integer like 401 etc.:

const graphqlKoaMiddleware = graphqlKoa(ctx => {
return ({
schema,
response: request.resonse,
formatError: (err) => {
response.statusCode =  err.status;
return ({message: err.message, status: err.status})},
context: {
  stationConnector: new StationConnector(),
  passengerTypeConnector: new PassengerTypeConnector(),
  authConnector: new AuthConnector(),
  cookies: ctx.cookies
}
})})

Upvotes: 0

larsivi
larsivi

Reputation: 1505

For apollo-server, install the apollo-server-errors package. For authentication errors,

import { AuthenticationError } from "apollo-server-errors";

Then, in your resolver throw new AuthenticationError('unknown user');

This will return a 400 status code.

Read more about this topic in this blog

Upvotes: 3

Daniel Rearden
Daniel Rearden

Reputation: 84867

Unless the GraphQL request itself is malformed, GraphQL will return a 200 status code, even if an error is thrown inside one of the resolvers. This is by design so there's not really a way to configure Apollo server to change this behavior.

That said, you could easily wire up your own middleware. You can import the runHttpQuery function that the Apollo middleware uses under the hood. In fact, you could pretty much copy the source code and just modify it to suit your needs:

const graphqlMiddleware = options => {
  return (req, res, next) => {
    runHttpQuery([req, res], {
      method: req.method,
      options: options,
      query: req.method === 'POST' ? req.body : req.query,
    }).then((gqlResponse) => {
      res.setHeader('Content-Type', 'application/json')

      // parse the response for errors and set status code if needed

      res.write(gqlResponse)
      res.end()
      next()
    }, (error) => {
      if ( 'HttpQueryError' !== error.name ) {
        return next(error)
      }

      if ( error.headers ) {
        Object.keys(error.headers).forEach((header) => {
          res.setHeader(header, error.headers[header])
        })
      }

      res.statusCode = error.statusCode
      res.write(error.message)
      res.end()
      next(false)
    })
  }
}

Upvotes: 8

Uziel Valdez
Uziel Valdez

Reputation: 2280

as you can see here formatError doesn't support status code, what you could do is create a status response type with message and status fields and return the corresponding on your resolver.

Does GraphQL require a 200 response even for failed queries / mutations? No, if the query fails it will return null and the error that you throw in the server side.

Upvotes: 1

Related Questions