如何在 GraphQL 中设置 http 状态码

How to set http status code in GraphQL

我想在我的 GraphQL 身份验证查询中设置一个 http 状态代码,这取决于身份验证尝试是成功 (200)、未授权 (401) 还是缺少参数 (422)。

我正在使用 Koa and Apollo 并像这样配置我的服务器:

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)

如您所见,我已将我的 formatError 设置为 return 消息和状态,但目前只有消息得到 returned。错误消息来自我在解析器函数中抛出的错误。

例如:

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}`);
      }
    }
  }
}

我对这种方法的唯一问题是它在错误消息中设置状态代码,而不是实际更新响应对象。

GraphQL 是否需要 200 响应,即使对于失败的查询/突变,或者我可以更新响应对象状态代码吗?如果不是,如何设置上述错误对象状态码?

如您所见,here formatError 不支持状态代码,您可以做的是创建一个状态响应类型,其中包含消息和状态字段以及 return 对应的你的解析器。

GraphQL 是否需要 200 响应,即使对于失败的查询/突变也是如此? 不,如果查询失败,它将 return null 和您在服务器端抛出的错误。

除非 GraphQL 请求本身格式不正确,否则 GraphQL 将 return 一个 200 状态代码,即使在其中一个解析器中抛出错误也是如此。这是设计使然,因此实际上没有办法配置 Apollo 服务器来更改此行为。

也就是说,您可以轻松连接自己的中间件。您可以导入 Apollo 中间件在后台使用的 runHttpQuery 函数。事实上,您几乎可以复制 source code 并根据您的需要修改它:

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)
    })
  }
}

对于 apollo-server,安装 apollo-server-errors 包。对于身份验证错误,

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

然后,在你的解析器中 throw new AuthenticationError('unknown user');

这将 return 一个 400 状态代码。

this blog

中阅读有关此主题的更多信息

尝试添加响应并设置响应状态代码,假设您的 err.status 已经是一个整数,如 401 等:

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
}
})})

根据 Daniels 的回答,我已经成功编写了中间件。

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));

apollo-server-express V3 支持。创建自己的插件。然后您可以查看抛出的错误以确定状态代码。

  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;