From 751dbf04aef69179a7eb800801e3f3eebee7bfb4 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 9 Apr 2021 12:33:27 -0700 Subject: [PATCH] apollo-server-lambda: Explicitly support payloadFormatVersion 2.0 (#5098) Fixes #5084. The sooner we get out of the business of understanding Lambda event formats, the better. But this fix should be good for now. --- CHANGELOG.md | 1 + .../apollo-server-lambda/src/ApolloServer.ts | 67 ++++++++++++------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdd608ebb53..8bc2be4d321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The version headers in this history reflect the versions of Apollo Server itself - `apollo-server-core`: Fix type for `formatResponse` function. It never is called with a `null` argument, and is allowed to return `null`. [Issue #5009](https://github.com/apollographql/apollo-server/issues/5009) [PR #5089](https://github.com/apollographql/apollo-server/pull/5089) - `apollo-server-lambda`: Fix regression in v2.21.2 where thrown errors were replaced by throwing the JS Error class itself. [PR #5085](https://github.com/apollographql/apollo-server/pull/5085) - `apollo-server-core`: If a client sends a variable of the wrong type, this is now reported as an error with an `extensions.code` of `BAD_USER_INPUT` rather than `INTERNAL_SERVER_ERROR`. [PR #5091](https://github.com/apollographql/apollo-server/pull/5091) [Issue #3498](https://github.com/apollographql/apollo-server/issues/3498) +- `apollo-server-lambda`: Explicitly support API Gateway `payloadFormatVersion` 2.0. Previously some codepaths did appropriate checks to partially support 2.0 and other codepaths could lead to errors like `event.path.endsWith is not a function` (especially since v2.21.1). Note that this changes the TypeScript typing of the `onHealthCheck` callback passed to `createHandler` to indicate that it can receive either type of event. If you are using TypeScript and care about having a precise typing for the argument to your `onHealthCheck` callback, you should determine which payload format you want to support and write `new ApolloServer(...)` or `new ApolloServer(...)` (importing these types from `aws-lambda`), or differentiate between the two formats by checking to see if `'path' in event`. [Issue #5084](https://github.com/apollographql/apollo-server/issues/5084) [Issue #5016](https://github.com/apollographql/apollo-server/issues/5016) ## v2.22.2 diff --git a/packages/apollo-server-lambda/src/ApolloServer.ts b/packages/apollo-server-lambda/src/ApolloServer.ts index c065bd8c674..2d0a52dede9 100644 --- a/packages/apollo-server-lambda/src/ApolloServer.ts +++ b/packages/apollo-server-lambda/src/ApolloServer.ts @@ -1,6 +1,7 @@ import { APIGatewayProxyCallback, APIGatewayProxyEvent, + APIGatewayProxyEventV2, APIGatewayProxyResult, Context as LambdaContext, } from 'aws-lambda'; @@ -22,7 +23,27 @@ import { ServerResponse, IncomingHttpHeaders, IncomingMessage } from 'http'; import { Headers } from 'apollo-server-env'; import { Readable, Writable } from 'stream'; -export interface CreateHandlerOptions { +// We try to support payloadFormatEvent 1.0 and 2.0. See +// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html +// for a bit of documentation as to what is in these objects. You can determine +// which one you have by checking `'path' in event` (V1 has path, V2 doesn't). +export type APIGatewayProxyEventV1OrV2 = APIGatewayProxyEvent | APIGatewayProxyEventV2; + +function eventHttpMethod(event: APIGatewayProxyEventV1OrV2): string { + return 'httpMethod' in event + ? event.httpMethod + : event.requestContext.http.method; +} + +function eventPath(event: APIGatewayProxyEventV1OrV2): string { + // Note: it's unclear if the V2 version should use `event.rawPath` or + // `event.requestContext.http.path`; I can't find any documentation about the + // distinction between the two. I'm choosing rawPath because that's what + // @vendia/serverless-express does (though it also looks at a `requestPath` + // field that doesn't exist in the docs or typings). + return 'path' in event ? event.path : event.rawPath; +} +export interface CreateHandlerOptions { cors?: { origin?: boolean | string | string[]; methods?: string | string[]; @@ -32,7 +53,7 @@ export interface CreateHandlerOptions { maxAge?: number; }; uploadsConfig?: FileUploadOptions; - onHealthCheck?: (req: APIGatewayProxyEvent) => Promise; + onHealthCheck?: (req: EventT) => Promise; } export class FileUploadRequest extends Readable { @@ -54,18 +75,18 @@ export class FileUploadRequest extends Readable { // // (Apollo Server 3 will drop Node 6 support, at which point we should just make // this package always return an async handler.) -function maybeCallbackify( +function maybeCallbackify( asyncHandler: ( - event: APIGatewayProxyEvent, + event: EventT, context: LambdaContext, ) => Promise, ): ( - event: APIGatewayProxyEvent, + event: EventT, context: LambdaContext, callback: APIGatewayProxyCallback | undefined, ) => void | Promise { return ( - event: APIGatewayProxyEvent, + event: EventT, context: LambdaContext, callback: APIGatewayProxyCallback | undefined, ) => { @@ -82,7 +103,7 @@ function maybeCallbackify( }; } -export class ApolloServer extends ApolloServerBase { +export class ApolloServer extends ApolloServerBase { protected serverlessFramework(): boolean { return true; } @@ -96,14 +117,14 @@ export class ApolloServer extends ApolloServerBase { // provides typings for the integration specific behavior, ideally this would // be propagated with a generic to the super class createGraphQLServerOptions( - event: APIGatewayProxyEvent, + event: EventT, context: LambdaContext, ): Promise { return super.graphQLServerOptions({ event, context }); } public createHandler( - { cors, onHealthCheck }: CreateHandlerOptions = { + { cors, onHealthCheck }: CreateHandlerOptions = { cors: undefined, onHealthCheck: undefined, }, @@ -152,9 +173,9 @@ export class ApolloServer extends ApolloServerBase { } } - return maybeCallbackify( + return maybeCallbackify( async ( - event: APIGatewayProxyEvent, + event: EventT, context: LambdaContext, ): Promise => { const eventHeaders = new Headers(event.headers); @@ -202,7 +223,7 @@ export class ApolloServer extends ApolloServerBase { return headersObject; }, {}); - if (event.httpMethod === 'OPTIONS') { + if (eventHttpMethod(event) === 'OPTIONS') { return { body: '', statusCode: 204, @@ -212,7 +233,7 @@ export class ApolloServer extends ApolloServerBase { }; } - if (event.path.endsWith('/.well-known/apollo/server-health')) { + if (eventPath(event).endsWith('/.well-known/apollo/server-health')) { if (onHealthCheck) { try { await onHealthCheck(event); @@ -237,14 +258,11 @@ export class ApolloServer extends ApolloServerBase { }; } - if (this.playgroundOptions && event.httpMethod === 'GET') { + if (this.playgroundOptions && eventHttpMethod(event) === 'GET') { const acceptHeader = event.headers['Accept'] || event.headers['accept']; if (acceptHeader && acceptHeader.includes('text/html')) { - const path = - event.path || - (event.requestContext && event.requestContext.path) || - '/'; + const path = eventPath(event) || '/'; const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { endpoint: path, @@ -310,7 +328,7 @@ export class ApolloServer extends ApolloServerBase { body = Buffer.from(body, 'base64').toString(); } - if (event.httpMethod === 'POST' && !body) { + if (eventHttpMethod(event) === 'POST' && !body) { return { body: 'POST body missing.', statusCode: 500, @@ -319,13 +337,14 @@ export class ApolloServer extends ApolloServerBase { if (bodyFromFileUploads) { query = bodyFromFileUploads; - } else if (body && event.httpMethod === 'POST' && isMultipart) { + } else if (body && eventHttpMethod(event) === 'POST' && isMultipart) { // XXX Not clear if this was only intended to handle the uploads // case or if it had more general applicability query = body as any; - } else if (body && event.httpMethod === 'POST') { + } else if (body && eventHttpMethod(event) === 'POST') { query = JSON.parse(body); } else { + // XXX Note that query = event.queryStringParameters || {}; } @@ -333,14 +352,14 @@ export class ApolloServer extends ApolloServerBase { const { graphqlResponse, responseInit } = await runHttpQuery( [event, context], { - method: event.httpMethod, + method: eventHttpMethod(event), options: async () => { return this.createGraphQLServerOptions(event, context); }, query, request: { - url: event.path, - method: event.httpMethod, + url: eventPath(event), + method: eventHttpMethod(event), headers: eventHeaders, }, },