Search code examples
hapi.jsapollo-server

Apollo Server & 4xx status codes


Currently, my Apollo Server(running on HapiJS) returns HTTP 200 for every request, including failed ones.

I would like the GraphQL server to return HTTP 4xx for unsuccessful requests. The primary reason for it is that I want to set up monitoring for my ELB.

I know that Apollo Server has an engine platform, but I want to implement it using my current infrastructure.

Any ideas of how I could accomplish that? I tried to capture 'onPreResponse' event for my HapiJS server but I couldn't modify status code there.


Solution

  • After reading this answer. Here is a solution by modifying the hapijs plugin graphqlHapi of hapiApollo.ts file.

    server.ts:

    import { makeExecutableSchema } from 'apollo-server';
    import { ApolloServer, gql } from 'apollo-server-hapi';
    import Hapi from 'hapi';
    import { graphqlHapi } from './hapiApollo';
    
    const typeDefs = gql`
      type Query {
        _: String
      }
    `;
    const resolvers = {
      Query: {
        _: () => {
          throw new Error('some error');
        },
      },
    };
    const schema = makeExecutableSchema({ typeDefs, resolvers });
    const port = 3000;
    async function StartServer() {
      const app = new Hapi.Server({ port });
      graphqlHapi.register(app, { path: '/graphql', graphqlOptions: { schema } });
      app.ext('onPreResponse', (request: any, h: any) => {
        const response = request.response;
        if (!response.isBoom) {
          return h.continue;
        }
        return h.response({ message: response.message }).code(400);
      });
    
      await app.start();
    }
    
    StartServer()
      .then(() => {
        console.log(`apollo server is listening on http://localhost:${port}/graphql`);
      })
      .catch((error) => console.log(error));
    

    hapiApollo.ts:

    import Boom from 'boom';
    import { Server, Request, RouteOptions } from 'hapi';
    import { GraphQLOptions, runHttpQuery, convertNodeHttpToRequest } from 'apollo-server-core';
    import { ValueOrPromise } from 'apollo-server-types';
    
    export interface IRegister {
      (server: Server, options: any, next?: Function): void;
    }
    
    export interface IPlugin {
      name: string;
      version?: string;
      register: IRegister;
    }
    
    export interface HapiOptionsFunction {
      (request?: Request): ValueOrPromise<GraphQLOptions>;
    }
    
    export interface HapiPluginOptions {
      path: string;
      vhost?: string;
      route?: RouteOptions;
      graphqlOptions: GraphQLOptions | HapiOptionsFunction;
    }
    
    const graphqlHapi: IPlugin = {
      name: 'graphql',
      register: (server: Server, options: HapiPluginOptions, next?: Function) => {
        if (!options || !options.graphqlOptions) {
          throw new Error('Apollo Server requires options.');
        }
        server.route({
          method: ['GET', 'POST'],
          path: options.path || '/graphql',
          vhost: options.vhost || undefined,
          options: options.route || {},
          handler: async (request, h) => {
            try {
              const { graphqlResponse, responseInit } = await runHttpQuery([request, h], {
                method: request.method.toUpperCase(),
                options: options.graphqlOptions,
                query:
                  request.method === 'post'
                    ? // TODO type payload as string or Record
                      (request.payload as any)
                    : request.query,
                request: convertNodeHttpToRequest(request.raw.req),
              });
    
              // add our custom error handle logic
              const graphqlResponseObj = JSON.parse(graphqlResponse);
              if (graphqlResponseObj.errors && graphqlResponseObj.errors.length) {
                throw new Error(graphqlResponseObj.errors[0].message);
              }
    
              const response = h.response(graphqlResponse);
              Object.keys(responseInit.headers as any).forEach((key) =>
                response.header(key, (responseInit.headers as any)[key]),
              );
              return response;
            } catch (error) {
              // handle our custom error
              if (!error.name) {
                throw Boom.badRequest(error.message);
              }
    
              if ('HttpQueryError' !== error.name) {
                throw Boom.boomify(error);
              }
    
              if (true === error.isGraphQLError) {
                const response = h.response(error.message);
                response.code(error.statusCode);
                response.type('application/json');
                return response;
              }
    
              const err = new Boom(error.message, { statusCode: error.statusCode });
              if (error.headers) {
                Object.keys(error.headers).forEach((header) => {
                  err.output.headers[header] = error.headers[header];
                });
              }
              // Boom hides the error when status code is 500
              err.output.payload.message = error.message;
              throw err;
            }
          },
        });
    
        if (next) {
          next();
        }
      },
    };
    
    export { graphqlHapi };
    

    Now, when the GraphQL resolver throws an error, the client-side will receive our custom response with Http status code 400 instead of 200 status code with GraphQL errors response.

    General from the browser:

    Request URL: http://localhost:3000/graphql
    Request Method: POST
    Status Code: 400 Bad Request
    Remote Address: 127.0.0.1:3000
    Referrer Policy: no-referrer-when-downgrade
    

    The response body is: {"message":"some error"}