我想在 GraphQL 身份验证查询中设置一个 http 状态代码,具体取决于身份验证尝试是否成功 (200)、未经授权 (401) 或缺少参数 (422)。
我正在使用 Koa 和 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
设置为返回消息和状态,但目前仅返回消息。该错误消息来自我在解析器函数中抛出的错误。
例如:
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
响应,或者我可以如何更新响应对象状态代码?如果没有,如何设置上述错误对象状态代码?
除非 GraphQL 请求本身格式错误,否则 GraphQL 将返回 200 状态代码,即使解析器之一内部抛出错误也是如此。这是设计使然,因此实际上没有办法配置 Apollo 服务器来改变这种行为。
也就是说,您可以轻松连接自己的中间件。您可以导入 Apollo 中间件在后台使用的
runHttpQuery
函数。事实上,您几乎可以复制源代码并修改它以满足您的需求:
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');
这将返回 400 状态代码。
在此博客
中阅读有关此主题的更多信息如您所见here
formatError
不支持状态代码,您可以做的是创建一个包含消息和状态字段的状态响应类型,并在解析器上返回相应的内容。
即使对于失败的查询/突变,GraphQL 是否也需要 200 响应? 不,如果查询失败,它将返回
null
以及您在服务器端抛出的错误。
尝试添加响应并设置响应状态代码,假设您的 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
}
})})
根据丹尼尔斯的回答,我已经成功编写了中间件。
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;
您定义的 setHttpPlugin 是一个自定义的 Apollo Server 插件。 Apollo Server 提供了一个插件系统,允许您通过定义自定义插件来扩展其功能。
在 setHttpPlugin 中,您定义一个插件,该插件根据响应中是否存在错误来修改 HTTP 响应状态代码。
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,
},
},
})
}}