Skip to content

Commit

Permalink
apollo-server-lambda: Explicitly support payloadFormatVersion 2.0 (#5098
Browse files Browse the repository at this point in the history
)

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.
  • Loading branch information
glasser committed Apr 9, 2021
1 parent f772ed6 commit 751dbf0
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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<APIGatewayProxyEvent>(...)` or `new ApolloServer<APIGatewayProxyEventV2>(...)` (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

Expand Down
67 changes: 43 additions & 24 deletions packages/apollo-server-lambda/src/ApolloServer.ts
@@ -1,6 +1,7 @@
import {
APIGatewayProxyCallback,
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
Context as LambdaContext,
} from 'aws-lambda';
Expand All @@ -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<EventT extends APIGatewayProxyEventV1OrV2 = APIGatewayProxyEventV1OrV2> {
cors?: {
origin?: boolean | string | string[];
methods?: string | string[];
Expand All @@ -32,7 +53,7 @@ export interface CreateHandlerOptions {
maxAge?: number;
};
uploadsConfig?: FileUploadOptions;
onHealthCheck?: (req: APIGatewayProxyEvent) => Promise<any>;
onHealthCheck?: (req: EventT) => Promise<any>;
}

export class FileUploadRequest extends Readable {
Expand All @@ -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<EventT extends APIGatewayProxyEventV1OrV2>(
asyncHandler: (
event: APIGatewayProxyEvent,
event: EventT,
context: LambdaContext,
) => Promise<APIGatewayProxyResult>,
): (
event: APIGatewayProxyEvent,
event: EventT,
context: LambdaContext,
callback: APIGatewayProxyCallback | undefined,
) => void | Promise<APIGatewayProxyResult> {
return (
event: APIGatewayProxyEvent,
event: EventT,
context: LambdaContext,
callback: APIGatewayProxyCallback | undefined,
) => {
Expand All @@ -82,7 +103,7 @@ function maybeCallbackify(
};
}

export class ApolloServer extends ApolloServerBase {
export class ApolloServer<EventT extends APIGatewayProxyEventV1OrV2 = APIGatewayProxyEventV1OrV2> extends ApolloServerBase {
protected serverlessFramework(): boolean {
return true;
}
Expand All @@ -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<GraphQLOptions> {
return super.graphQLServerOptions({ event, context });
}

public createHandler(
{ cors, onHealthCheck }: CreateHandlerOptions = {
{ cors, onHealthCheck }: CreateHandlerOptions<EventT> = {
cors: undefined,
onHealthCheck: undefined,
},
Expand Down Expand Up @@ -152,9 +173,9 @@ export class ApolloServer extends ApolloServerBase {
}
}

return maybeCallbackify(
return maybeCallbackify<EventT>(
async (
event: APIGatewayProxyEvent,
event: EventT,
context: LambdaContext,
): Promise<APIGatewayProxyResult> => {
const eventHeaders = new Headers(event.headers);
Expand Down Expand Up @@ -202,7 +223,7 @@ export class ApolloServer extends ApolloServerBase {
return headersObject;
}, {});

if (event.httpMethod === 'OPTIONS') {
if (eventHttpMethod(event) === 'OPTIONS') {
return {
body: '',
statusCode: 204,
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -319,28 +337,29 @@ 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 || {};
}

try {
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,
},
},
Expand Down

0 comments on commit 751dbf0

Please sign in to comment.