Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apollo-server-lambda: createHandler's handler doesn't take a callback #5188

Merged
merged 1 commit into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The version headers in this history reflect the versions of Apollo Server itself
- The `schemaDirectives` option to `new ApolloServer` has been removed. This option was implemented inside the `graphql-tools` function `makeExecutableSchema`. If you'd like to continue using it, just import `makeExecutableSchema` from `@graphql-tools/schema` and call it yourself: `new ApolloServer({schema: makeExecutableSchema({typeDefs, resolvers, schemaDirectives})})`. Note that `graphql-tools` calls this option ["legacy" schema directives](https://www.graphql-tools.com/docs/legacy-schema-directives/) and you may want to consider the newer [`schemaTransforms`](https://www.graphql-tools.com/docs/schema-directives/) option instead.
- The `mocks` option to `new ApolloSchema` is now powered by `@graphql-tools/mock` v7 instead of `graphql-tools` v4. This contains some [breaking changes](https://www.graphql-tools.com/docs/mocking#migration-from-v7-and-below); for example, mock functions no longer receive arguments and cannot return `Promise`s. Note that some of the suggestions in the v7 migration guide suggest using the `resolvers` argument to `addMocksToSchema`. Apollo Server does not support this option, but you're welcome to call `addMocksToSchema` yourself and pass the result to the `schema` option to `new ApolloSchema`.
- `apollo-server-express`: We no longer officially support using this package with the `connect` framework. We have not actively removed any `connect` compatibility code and do still test that it works with `connect`, but we reserve the right to break that compatibility without a major version bump of this package (though it will certainly be noted in the CHANGELOG if we do so).
- `apollo-server-lambda`: The handler returned by `createHandler` can now only be called as an async function returning a `Promise` (it no longer optionally accepts a callback as the third argument). All current Lambda Node runtimes support this invocation mode (so `exports.handler = server.createHandler()` will keep working without any changes), but if you've written your own handler which calls the handler returned by `createHandler` with a callback, you'll need to handle its `Promise` return value instead.
- Top-level exports have changed. E.g.,

- We no longer re-export the entirety of `graphql-tools` (including `makeExecutableSchema`) from all Apollo Server packages. If you'd like to continue using them, install [`graphql-tools`](https://www.graphql-tools.com/) or one of its sub-packages yourself.
Expand Down
325 changes: 137 additions & 188 deletions packages/apollo-server-lambda/src/ApolloServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
APIGatewayProxyCallback,
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
Expand Down Expand Up @@ -53,50 +52,6 @@ export interface CreateHandlerOptions<
};
onHealthCheck?: (req: EventT) => Promise<any>;
}

// Lambda has two ways of defining a handler: as an async Promise-returning
// function, and as a callback-invoking function.
// https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html The async
// variety was introduced with Lambda's Node 8 runtime. Apparently the
// callback-invoking variety was removed with their Node 14 runtime (their docs
// don't mention this anywhere but our users have reported this:
// https://github.com/apollographql/apollo-server/issues/1989#issuecomment-778982945).
// While AWS doesn't directly support pre-Node-8 runtimes any more, it's
// possible some users are using a Custom Runtime that still requires the Node 6
// version, and Apollo Server still technically supports Node 6. So for now, we
// define an async handler and use this function to convert it to a function
// that can work either as an async or callback handler.
//
// (Apollo Server 3 will drop Node 6 support, at which point we should just make
// this package always return an async handler.)
function maybeCallbackify<EventT extends APIGatewayProxyEventV1OrV2>(
asyncHandler: (
event: EventT,
context: LambdaContext,
) => Promise<APIGatewayProxyResult>,
): (
event: EventT,
context: LambdaContext,
callback: APIGatewayProxyCallback | undefined,
) => void | Promise<APIGatewayProxyResult> {
return (
event: EventT,
context: LambdaContext,
callback: APIGatewayProxyCallback | undefined,
) => {
if (callback) {
context.callbackWaitsForEmptyEventLoop = false;
asyncHandler(event, context).then(
(r: APIGatewayProxyResult) => callback(null, r),
(e) => callback(e),
);
return;
} else {
return asyncHandler(event, context);
}
};
}

export class ApolloServer<
EventT extends APIGatewayProxyEventV1OrV2 = APIGatewayProxyEventV1OrV2
> extends ApolloServerBase {
Expand Down Expand Up @@ -164,174 +119,168 @@ export class ApolloServer<
}
}

return maybeCallbackify<EventT>(
async (
event: EventT,
context: LambdaContext,
): Promise<APIGatewayProxyResult> => {
const eventHeaders = new Headers(event.headers);

// Make a request-specific copy of the CORS headers, based on the server
// global CORS headers we've set above.
const requestCorsHeaders = new Headers(corsHeaders);

if (cors && cors.origin) {
const requestOrigin = eventHeaders.get('origin');
if (typeof cors.origin === 'string') {
requestCorsHeaders.set('access-control-allow-origin', cors.origin);
} else if (
requestOrigin &&
(typeof cors.origin === 'boolean' ||
(Array.isArray(cors.origin) &&
requestOrigin &&
cors.origin.includes(requestOrigin)))
) {
requestCorsHeaders.set(
'access-control-allow-origin',
requestOrigin,
);
}

const requestAccessControlRequestHeaders = eventHeaders.get(
'access-control-request-headers',
);
if (!cors.allowedHeaders && requestAccessControlRequestHeaders) {
requestCorsHeaders.set(
'access-control-allow-headers',
requestAccessControlRequestHeaders,
);
}
}
return async (
event: EventT,
context: LambdaContext,
): Promise<APIGatewayProxyResult> => {
const eventHeaders = new Headers(event.headers);

// Convert the `Headers` into an object which can be spread into the
// various headers objects below.
// Note: while Object.fromEntries simplifies this code, it's only currently
// supported in Node 12 (we support >=6)
const requestCorsHeadersObject = Array.from(requestCorsHeaders).reduce<
Record<string, string>
>((headersObject, [key, value]) => {
headersObject[key] = value;
return headersObject;
}, {});
// Make a request-specific copy of the CORS headers, based on the server
// global CORS headers we've set above.
const requestCorsHeaders = new Headers(corsHeaders);

if (eventHttpMethod(event) === 'OPTIONS') {
return {
body: '',
statusCode: 204,
headers: {
...requestCorsHeadersObject,
},
};
if (cors && cors.origin) {
const requestOrigin = eventHeaders.get('origin');
if (typeof cors.origin === 'string') {
requestCorsHeaders.set('access-control-allow-origin', cors.origin);
} else if (
requestOrigin &&
(typeof cors.origin === 'boolean' ||
(Array.isArray(cors.origin) &&
requestOrigin &&
cors.origin.includes(requestOrigin)))
) {
requestCorsHeaders.set('access-control-allow-origin', requestOrigin);
}

if (eventPath(event).endsWith('/.well-known/apollo/server-health')) {
if (onHealthCheck) {
try {
await onHealthCheck(event);
} catch (_) {
return {
body: JSON.stringify({ status: 'fail' }),
statusCode: 503,
headers: {
'Content-Type': 'application/json',
...requestCorsHeadersObject,
},
};
}
}
return {
body: JSON.stringify({ status: 'pass' }),
statusCode: 200,
headers: {
'Content-Type': 'application/json',
...requestCorsHeadersObject,
},
};
const requestAccessControlRequestHeaders = eventHeaders.get(
'access-control-request-headers',
);
if (!cors.allowedHeaders && requestAccessControlRequestHeaders) {
requestCorsHeaders.set(
'access-control-allow-headers',
requestAccessControlRequestHeaders,
);
}
}

if (this.playgroundOptions && eventHttpMethod(event) === 'GET') {
const acceptHeader =
event.headers['Accept'] || event.headers['accept'];
if (acceptHeader && acceptHeader.includes('text/html')) {
const path = eventPath(event) || '/';
// Convert the `Headers` into an object which can be spread into the
// various headers objects below.
// Note: while Object.fromEntries simplifies this code, it's only currently
// supported in Node 12 (we support >=6)
const requestCorsHeadersObject = Array.from(requestCorsHeaders).reduce<
Record<string, string>
>((headersObject, [key, value]) => {
headersObject[key] = value;
return headersObject;
}, {});

const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
endpoint: path,
...this.playgroundOptions,
};
if (eventHttpMethod(event) === 'OPTIONS') {
return {
body: '',
statusCode: 204,
headers: {
...requestCorsHeadersObject,
},
};
}

if (eventPath(event).endsWith('/.well-known/apollo/server-health')) {
if (onHealthCheck) {
try {
await onHealthCheck(event);
} catch (_) {
return {
body: renderPlaygroundPage(playgroundRenderPageOptions),
statusCode: 200,
body: JSON.stringify({ status: 'fail' }),
statusCode: 503,
headers: {
'Content-Type': 'text/html',
'Content-Type': 'application/json',
...requestCorsHeadersObject,
},
};
}
}
return {
body: JSON.stringify({ status: 'pass' }),
statusCode: 200,
headers: {
'Content-Type': 'application/json',
...requestCorsHeadersObject,
},
};
}

let { body, isBase64Encoded } = event;
let query: Record<string, any> | Record<string, any>[];

if (body && isBase64Encoded) {
body = Buffer.from(body, 'base64').toString();
}
if (this.playgroundOptions && eventHttpMethod(event) === 'GET') {
const acceptHeader = event.headers['Accept'] || event.headers['accept'];
if (acceptHeader && acceptHeader.includes('text/html')) {
const path = eventPath(event) || '/';

if (eventHttpMethod(event) === 'POST' && !body) {
return {
body: 'POST body missing.',
statusCode: 500,
const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
endpoint: path,
...this.playgroundOptions,
};
}

if (body && eventHttpMethod(event) === 'POST') {
query = JSON.parse(body);
} else {
// XXX Note that if a parameter is included multiple times, this only
// includes the first version for payloadFormatVersion 1.0 but
// contains all of them joined with commas for payloadFormatVersion
// 2.0.
query = event.queryStringParameters || {};
}

try {
const { graphqlResponse, responseInit } = await runHttpQuery(
[event, context],
{
method: eventHttpMethod(event),
options: async () => {
return this.createGraphQLServerOptions(event, context);
},
query,
request: {
url: eventPath(event),
method: eventHttpMethod(event),
headers: eventHeaders,
},
},
);
return {
body: graphqlResponse,
body: renderPlaygroundPage(playgroundRenderPageOptions),
statusCode: 200,
headers: {
...responseInit.headers,
...requestCorsHeadersObject,
},
};
} catch (error) {
if (error.name !== 'HttpQueryError') throw error;
const httpQueryError = error as HttpQueryError;
return {
body: httpQueryError.message,
statusCode: httpQueryError.statusCode,
headers: {
...httpQueryError.headers,
'Content-Type': 'text/html',
...requestCorsHeadersObject,
},
};
}
},
);
}

let { body, isBase64Encoded } = event;
let query: Record<string, any> | Record<string, any>[];

if (body && isBase64Encoded) {
body = Buffer.from(body, 'base64').toString();
}

if (eventHttpMethod(event) === 'POST' && !body) {
return {
body: 'POST body missing.',
statusCode: 500,
};
}

if (body && eventHttpMethod(event) === 'POST') {
query = JSON.parse(body);
} else {
// XXX Note that if a parameter is included multiple times, this only
// includes the first version for payloadFormatVersion 1.0 but
// contains all of them joined with commas for payloadFormatVersion
// 2.0.
query = event.queryStringParameters || {};
}

try {
const { graphqlResponse, responseInit } = await runHttpQuery(
[event, context],
{
method: eventHttpMethod(event),
options: async () => {
return this.createGraphQLServerOptions(event, context);
},
query,
request: {
url: eventPath(event),
method: eventHttpMethod(event),
headers: eventHeaders,
},
},
);
return {
body: graphqlResponse,
statusCode: 200,
headers: {
...responseInit.headers,
...requestCorsHeadersObject,
},
};
} catch (error) {
if (error.name !== 'HttpQueryError') throw error;
const httpQueryError = error as HttpQueryError;
return {
body: httpQueryError.message,
statusCode: httpQueryError.statusCode,
headers: {
...httpQueryError.headers,
...requestCorsHeadersObject,
},
};
}
};
}
}
Loading