错误处理
在客户端和服务器上使错误可操作
在 Apollo Server v4 中引入了一个回归,即提供无效的status400ForVariableCoercionErrors: true
stacktrace
__typename
为了帮助调试,Apollo Server 提供了一个ApolloServerErrorCode
枚举类型,您可以使用它来检测您遇到的错误是否是Apollo Server生成的不同类型的错误之一。
您可以通过检查一个错误的code
来确定错误发生的原因,并为不同类型的错误添加逻辑进行响应,如下所示:
import { ApolloServerErrorCode } from '@apollo/server/errors';if (error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED) {// respond to the syntax error} else if (error.extensions?.code === "MY_CUSTOM_CODE") {// do something else}
Apollo Server的多种错误代码使得请求客户端能够针对不同类型的错误做出不同的响应。您还可以创建自己的自定义错误和代码。
内置错误代码
代码 | 描述 |
---|---|
| GraphQL操作字符串包含语法错误。 |
| GraphQL操作不符合服务器的模式。 |
| GraphQL操作包括一个无效的字段参数值。 |
| 客户端发送了一个要执行的查询字符串的哈希值,但查询不在APQ缓存中。 |
| 客户端发送了一个要执行的查询字符串的哈希值,但服务器已禁用APQ。 |
| 请求已成功解析并在服务器的模式下有效,但服务器无法解析要运行的操作。 当请求包含多个已命名的操作,并未指定运行哪个操作时,或如果命名的操作未包含在请求中,就会发生这种情况。 |
| 在服务器尝试解析给定的GraphQL操作之前发生了错误。 |
| 发生了一个未指定的错误。 当Apollo Server格式化响应中的错误时,如果未设置其他代码,则将其扩展设置为该值。 |
自定义错误
您可以使用 graphql
包的 GraphQLError
类来创建自定义错误和代码,例如:
import { GraphQLError } from 'graphql';throw new GraphQLError(message, {extensions: { code: 'YOUR_ERROR_CODE', myCustomExtensions },});
自定义错误可以提供额外的上下文,使您的客户端能够理解 错误发生的原因。我们建议为常见情况创建清晰的错误,例如,当用户未登录时(UNAUTHENTICATED
),或某人被禁止执行行动:
import { GraphQLError } from 'graphql';throw new GraphQLError('You are not authorized to perform this action.', {extensions: {code: 'FORBIDDEN',},});
抛出错误
Apollo Server 在适用时自动抛出 错误。例如,当传入的操作不符合服务器架构时,它会抛出一个 GRAPHQL_VALIDATION_FAILED
错误。
您的 解析器 也可以在 Apollo Server 自动不抛出错误的情况下抛出错误。
例如,这个 解析器 在提供给用户 ID 的整数值小于 1
时抛出一个自定义错误:
如果 解析器 � throw一个通用的错误,它不是一个 GraphQLError
实例,这个错误仍然会带有 extensions
字段,其中包含一个 stacktrace
和 code
(具体为 INTERNAL_SERVER_ERROR
),以及任何其他相关的错误详细信息。
包含自定义错误详情
每当您抛出 GraphQLError
时,您可以在错误的 extensions
对象中添加任意的 字段 来向客户端提供额外的上下文。您在这些字段中指定提供到错误构造函数的对象。
此示例基于上面的示例,通过添加无效的 GraphQL 参数的名称:
这会导致以下类似响应
省略或包含 stacktrace
stacktrace
错误字段在开发时非常有用,但在生产环境中您可能不想将其暴露给客户端。
默认情况下,如果 Apollo Server 省略 了 stacktrace
字段,则 NODE_ENV
环境变量设置为 production
或 test
。
您可以通过向ApolloServer
的构造函数传递includeStacktraceInErrorResponses
选项,来覆盖默认行为。如果includeStacktraceInErrorResponses
设置为true
,则始终包含stacktrace
。如果设置为false
,则始终不包含stacktrace
。
请注意,当省略stacktrace
时,它也对您的应用程序不可用。要记录错误stacktrace
而无需将其包含在客户端响应中,请参阅错误遮蔽和记录。
错误遮蔽和记录
您可以在将错误详细信息传递给客户端或报告给Apollo Studio之前编辑Apollo Server的错误。这使您可以省略敏感或不相关的数据。
对于客户端响应
ApolloServer构造函数接受一个formatError
钩子,该钩子在将错误传递回客户端之前运行。您可以使用此函数来记录或遮蔽特定错误。
formatError钩子接收两个参数:第一个是格式化为JSON对象的错误(将随响应一起发送),第二个是原始错误(如果由解析器抛出,则包装在GraphQLError
中)。
formatError函数不会修改作为使用情况报告部分发送到Apollo Studio的错误。有关Apollo Studio报告的信息。
以下示例在 Apollo Server抛出GRAPHQL_VALIDATION_FAILED
错误时返回友好的消息:
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloServerErrorCode } from '@apollo/server/errors';const server = new ApolloServer({typeDefs,resolvers,formatError: (formattedError, error) => {// Return a different error messageif (formattedError.extensions.code ===ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED) {return {...formattedError,message: "Your query doesn't match the schema. Try double-checking it!",};}// Otherwise return the formatted error. This error can also// be manipulated in other ways, as long as it's returned.return formattedError;},});const { url } = await startStandaloneServer(server);console.log(`🚀 Server listening at: ${url}`);
另一个示例,在这里,如果我们返回的原始错误的消息以Database Error:
开头,则返回更通用的错误:
formatError: (formattedError, error) => {if (formattedError.message.startsWith('Database Error: ')) {return { message: 'Internal server error' };}// Otherwise return the formatted error.return formattedError;},
如果要根据原始错误(而不经过JSON格式化)访问,您可以使用formatError
的第二个参数。
例如,如果您在应用程序中使用数据库包,并且当您的服务器抛出特定类型的数据库错误时想要执行某些操作
formatError: (formattedError, error) => {if (error instanceof CustomDBError) {// do something specific}},
注意,如果一个 解析器 抛出错误,将围绕最初抛出的错误包装一个 GraphQLError
。这个 GraphQLError
会整齐地格式化错误并包含有用的 字段,例如错误发生的 path
。
如果你想要移除外部的 GraphQLError
以访问最初抛出的错误,你可以使用来自 @apollo/server/errors
的 unwrapResolverError
。这个 unwrapResolverError
函数可以从解析器错误中移除 GraphQLError
包装,或者在它不是来自解析器时返回未更改的错误。
因此,我们可以将上述代码片段修改为适用于在解析器和解析器外部抛出的错误,如下所示:
import { unwrapResolverError } from '@apollo/server/errors';new ApolloServer({formatError: (formattedError, error) => {// unwrapResolverError removes the outer GraphQLError wrapping from// errors thrown in resolvers, enabling us to check the instance of// the original errorif (unwrapResolverError(error) instanceof CustomDBError) {return { message: 'Internal server error' };}},});
要针对 formatError
收到的错误进行上下文特定的调整(如本地化或个性化),请考虑 创建一个插件,该插件使用 didEncounterErrors
生命周期事件将附加的属性附加到错误上。这些属性可以从 formatError
访问。
对于 Apollo Studio 报告
Apollo Server 4 的新功能:默认情况下,错误详情不包括在跟踪中。
您可以使用 Apollo Studio 分析您服务器的错误率。默认情况下,作为详细跟踪发送到 Studio 的操作不 包含错误详情。
如果您确实想将错误信息发送到 Studio,您可以将每个错误发送,或者您可以在传输之前修改或更正特定的错误。
要将所有错误发送到 Studio,您可以在 sendErrors
中传递 { unmodified: true }
,如下所示:
new ApolloServer({// etc.plugins: [ApolloServerPluginUsageReporting({// If you pass unmodified: true to the usage reporting// plugin, Apollo Studio receives ALL error detailssendErrors: { unmodified: true },}),],});
如果您想要报告特定的错误或修改错误然后报告它,您可以传递一个函数到 sendErrors.transform
选项,如下所示:
new ApolloServer({// etc.plugins: [ApolloServerPluginUsageReporting({sendErrors: {transform: (err) => {if (err.extensions.code === 'MY_CUSTOM_CODE') {// returning null will skip reporting this errorreturn null;}// All other errors are reported.return err;},},}),],});
如果您提供 Apollo API 密钥给 Apollo Server,则将自动安装使用情况报告插件,并使用默认配置。要自定义使用情况报告插件的功能,您需要显式使用自定义配置安装它,如下面的示例所示。
传递给 transform
的函数为要报告给 Studio 的每个错误(GraphQLError
)调用。该函数可以是:
- 返回一个修改过的错误形式(例如,通过更改
err.message
以移除可能敏感的信息) - 返回
null
以防止错误被完全报告
请注意,返回 null
也会影响 Studio 对操作中包含错误数量以及错误出现路径的聚合统计。
如上所述,您可以使用 unwrapResolverError
(来自 @apollo/server/errors
)来移除包裹原始错误的 GraphQLError
。
对于联邦图,在 子图 中定义您的 内联跟踪插件中的 transform
函数以重写字段错误。如果您想转换网关的解析或验证错误,您可以在网关中定义您的 transform
函数。
示例:忽略常见低严重性错误
假设我们的服务器在用户输入错误密码时抛出 throw
出了 UNAUTHENTICATED
错误。我们可以通过定义一个 transform
函数来避免将这些错误报告给 Apollo Studio,如下所示:
import { ApolloServer } from '@apollo/server';import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';const server = new ApolloServer({typeDefs,resolvers,plugins: [ApolloServerPluginUsageReporting({sendErrors: {transform: (err) => {// Return `null` to avoid reporting `UNAUTHENTICATED` errorsif (err.extensions.code === 'UNAUTHENTICATED') {return null;}// All other errors will be reported.return err;},},}),],});
此配置示例确保在 resolver 内抛出的任何 UNAUTHENTICATED
错误只报告给客户端,而永远不会发送到 Apollo Studio。所有其他错误按正常情况传输到 Studio。
示例:根据其他属性过滤错误
在生成一个错误(例如, new GraphQLError("Failure!")
)时,错误的 message
是最常见的扩展(在这种情况下它是 Failure!
)。然而,可以添加任意数量的扩展到错误中(例如, code
扩展)。
我们可以通过以下方式使用 transform
函数检查这些扩展以确定是否应将错误报告给 Apollo Studio:
import { ApolloServer } from '@apollo/server';import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';const server = new ApolloServer({typeDefs,resolvers,plugins: [ApolloServerPluginUsageReporting({sendErrors: {transform: (err) => {// Using a more stable, known error extension (e.g. `err.code`) would be// more defensive, however checking the `message` might serve most needs!if (err.message && err.message.startsWith('Known error message')) {return null;}// All other errors should still be reported!return err;},},}),],});
此配置示例确保任何以 Known error message
开始的错误不传输到 Apollo Studio,但所有其他错误按正常情况发送。
示例:从错误消息中删除信息
如上所述,默认情况下,作为详细跟踪发送到 Studio 的操作不包含错误详情。
如果您 确实 想要将错误详情发送到 Apollo Studio,但需要先删除一些信息, transform
函数可以帮助。
例如,如果错误 message
中含有个人可识别信息,例如 API 密钥:
import { GraphQLError } from 'graphql';throw new GraphQLError("The x-api-key:12345 doesn't have sufficient privileges.",);
此 transform
函数可以确保此类信息不会被发送到 Apollo Studio 并可能在其预期范围之外被泄露:
import { ApolloServer } from '@apollo/server';import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';const server = new ApolloServer({typeDefs,resolvers,plugins: [ApolloServerPluginUsageReporting({sendErrors: {transform: (err) => {// Make sure that a specific pattern is removed from all error messages.err.message = err.message.replace(/x-api-key:[A-Z0-9-]+/, 'REDACTED');return err;},},}),],});
在这种情况下,上述错误报告给 Apollo Studio 的方式是
The REDACTED doesn't have sufficient privileges.
设置HTTP状态码和头信息
GraphQL,按照设计,不使用REST中相同的约定(通过HTTP动词和状态码进行通信)。客户端信息应包含在模式中或作为标准响应
Apollo Server在的各种情况下使用不同的HTTP状态码:
- 如果Apollo Server启动不正确或在关闭过程中,它将返回500状态码。
- 这可能会发生在您使用无服务器集成并将其请求发送到启动时出现错误的Apollo Server实例时。后一种情况发生在您没有正确
- 如果Apollo Server无法将请求解析为合法的GraphQL
- 如果一个请求使用了无效的HTTP方法(带有
- 如果您的
- 如果在处理请求的过程中发生意外错误(无论是Apollo Server中的错误还是插件钩子抛出),Apollo Server将返回500状态码。
- 否则,Apollo Server返回200状态码。这基本上是在服务器可以执行GraphQL操作并成功完成执行的情况(尽管这仍然可能包括特定的求解器错误)。
要更改HTTP状态码或设置自定义响应头,您有以下三种方法:在一个求解器中抛出错误,在您的
虽然Apollo Server允许您根据解析器抛出的错误设置HTTP状态码,但对于通过HTTP进行GraphQL的最佳实践来说,我们在操作执行时始终发送200状态码更为鼓励。因此,我们不建议在解析器中使用这种机制,而只推荐在context
函数或请求管道早期阶段的插件中。
请注意,GraphQL客户端库可能不会对所有的响应状态码一视同仁,所以使用哪种模式将由您的团队决定。
要基于一个解析器或context
函数中抛出的错误,更改变量HTTP状态码和响应头,可以这样抛出一个带有http
扩展的GraphQLError
,如下所示:
import { GraphQLError } from 'graphql';const resolvers = {Query: {someField() {throw new GraphQLError('the error message', {extensions: {code: 'SOMETHING_BAD_HAPPENED',http: {status: 404,headers: new Map([['some-header', 'it was bad'],['another-header', 'seriously'],]),},},});},},};
import { GraphQLError } from 'graphql';const resolvers = {Query: {someField() {throw new GraphQLError('the error message', {extensions: {code: 'SOMETHING_BAD_HAPPENED',http: {status: 404,headers: new Map([['some-header', 'it was bad'],['another-header', 'seriously'],]),},},});},},};
除非您要覆盖默认状态码(解析器为200,context函数为500),您不需要包含status
。可选的headers
字段应提供一个包含小写头部名称的Map
。
如果您的配置包含多个解析器,它们抛出设置状态码或设置相同头的错误,Apollo Server可能会以任意方式解决这种冲突(这可能在未来的版本中发生变化)。我们建议编写一个插件(如以下所示)。
您也可以从插件设置HTTP状态码和头部。以下是如何根据GraphQL错误设置自定义响应头部和状态码的示例:
const setHttpPlugin = {async requestDidStart() {return {async willSendResponse({ response }) {response.http.headers.set('custom-header', 'hello');if (response.body.kind === 'single' &&response.body.singleResult.errors?.[0]?.extensions?.code === 'TEAPOT') {response.http.status = 418;}},};},};const server = new ApolloServer({typeDefs,resolvers,plugins: [setHttpPlugin],});