【发布时间】:2019-08-30 21:52:07
【问题描述】:
我正在尝试通过 Apollo Server Express 和 Passport JWT 使用 GraphQL 构建一个微服务 Web 应用程序示例以进行令牌身份验证。
到目前为止,我有 4 个微服务(用户、博客、项目、配置文件)和一个网关 API,我将它们与关系片段拼接在一起(例如 Blog.author 或 User.projects 等)。一切运行良好,我可以全面执行完整的 CRUD。
然后,当我尝试实现身份验证时,一切都陷入了困境(这让我大吃一惊),虽然奇怪的是没有实现身份验证本身,但这不是问题。
问题在于错误处理,更具体地说,是将 GraphQL 错误从远程 API 传递到网关以进行拼接。网关发现存在错误,但实际详细信息(例如 {password: 'password incorrect'})被网关 API 吞噬。
用户 API 错误
{
"errors": [
{
"message": "The request is invalid.",
"type": "ValidationError",
"state": {
"password": [
"password incorrect"
]
},
"path": [
"loginUser"
],
"stack": [
...
]
}
],
"data": {
"loginUser": null
}
}
网关 API 错误
{
"errors": [
{
"message": "The request is invalid.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"loginUser"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"errors": [
{
"message": "The request is invalid.",
"locations": [],
"path": [
"loginUser"
]
}
],
"stacktrace": [
"Error: The request is invalid.",
... // stacktrace refers to node_modules/graphql-
tools/src/stitching
],
"data": {
"loginUser": null
}
}
GATEWAY src/index.js 从“快递”进口快递
s';
import { ApolloServer } from 'apollo-server-express';
// ...
import errorHandler from '../error-handling/errorHandler';
// ... app setup
const startGateway = async () => {
const schema = await makeSchema(); // stitches schema
const app = express();
app.use('/graphql', (req, res, next) => {
// passport
// ...
});
const server = new ApolloServer({
schema,
context: ({ req }) => ({ authScope: req.headers.authorization }),
// custom error handler that tries to unravel, clean and return error
formatError: (err) => errorHandler(true)(err)
});
server.applyMiddleware({ app });
app.listen({ port: PORT }, () => console.log(`\n Gateway Server ready at http://localhost:${PORT}${server.graphqlPath} \n`));
};
startGateway().catch(err => console.log(err));
GATEWAY src/remoteSchema/index.js (缝合发生的地方)
import { makeRemoteExecutableSchema, introspectSchema } from 'graphql-tools';
import { ApolloLink } from 'apollo-link';
import { setContext } from 'apollo-link-context';
import { introspectionLink, stitchingLink } from './link';
// graphql API metadata
const graphqlApis = [
{ uri: config.USER_DEV_API },
{ uri: config.BLOG_DEV_API },
{ uri: config.PROJECT_DEV_API },
{ uri: config.PROFILE_DEV_API }
];
// create executable schemas from remote GraphQL APIs
export default async () => {
const schemas = [];
for (const api of graphqlApis) {
const contextLink = setContext((request, previousContext) => {
const { authScope } = previousContext.graphqlContext;
return {
headers: {
authorization: authScope
}
};
});
// INTROSPECTION LINK
const apiIntroSpectionLink = await introspectionLink(api.uri);
// INTROSPECT SCHEMA
const remoteSchema = await introspectSchema(apiIntroSpectionLink);
// STITCHING LINK
const apiSticthingLink = stitchingLink(api.uri);
// MAKE REMOTE SCHEMA
const remoteExecutableSchema = makeRemoteExecutableSchema({
schema: remoteSchema,
link: ApolloLink.from([contextLink, apiSticthingLink])
});
schemas.push(remoteExecutableSchema);
}
return schemas;
};
缝合还有更多,但这里太多了。但它缝合成功。
USER API 源代码/解析器
const resolvers = {
Query: {/*...*/},
Mutation: {
loginUser: async (parent, user) => {
const errorArray = [];
// ...get the data...
const valid = await bcrypt.compare(user.password, ifUser.password);
if (!valid) {
errorArray.push(validationError('password', 'password incorrect'));
// throws a formatted error in USER API but not handled in GATEWAY
throw new GraphQlValidationError(errorArray);
}
// ... return json web token if valid
}
}
}
用户错误.js
export class GraphQlValidationError extends GraphQLError {
constructor(errors) {
super('The request is invalid.');
this.state = errors.reduce((result, error) => {
if (Object.prototype.hasOwnProperty.call(result, error.key)) {
result[error.key].push(error.message);
} else {
result[error.key] = [error.message];
}
return result;
}, {});
this.type = errorTypes.VALIDATION_ERROR;
}
}
export const validationError = (key, message) => ({ key, message });
网关和用户 errorHandler.js
import formatError from './formatError';
export default includeStack => (error) => {
const formattedError = formatError(includeStack)(error);
return formattedError;
};
formatError.js
import errorTypes from './errorTypes';
import unwrapErrors from './unwrapErrors';
export default shouldIncludeStack => (error) => {
const unwrappedError = unwrapErrors(error);
const formattedError = {
message: unwrappedError.message || error.message,
type: unwrappedError.type || error.type || errorTypes.ERROR,
state: unwrappedError.state || error.state,
detail: unwrappedError.detail || error.detail,
path: unwrappedError.path || error.path,
};
if (shouldIncludeStack) {
formattedError.stack = unwrappedError.stack || error.extensions.exception.stacktrace;
}
return formattedError;
};
unwrapErrors.js
export default function unwrapErrors(err) {
if (err.extensions) {
return unwrapErrors(err.extensions);
}
if (err.exception) {
return unwrapErrors(err.exception);
}
if (err.errors) {
return unwrapErrors(err.errors);
}
return err;
}
如果代码 sn-ps 不是我们需要的,我提前道歉。我很乐意回答任何问题。
提前致谢!
【问题讨论】:
-
你能用你传递给
formatError的errorHandler函数更新问题吗? -
@DanielRearden 完成
标签: error-handling graphql microservices apollo-server