Skip to content

Commit

Permalink
lambda: Support case-insensitive headers in pre-request CORS logic.
Browse files Browse the repository at this point in the history
I hope this will be an alternative implementation for the contribution made
in #2597 by @dsanders11.

While that solution will certainly works, it expands on our existing
double-checking of multiple header CaSe-patterns in multiple locations,
prior to GraphQL execution beginning.

As a proposed alternative, rather than checking for two types of expected
header capitalization combinations in every one of our checks (which I
should note leaves out MaNy-OtHeR-Header-patterns which are perfectly valid
since HTTP headers are, by specification, case-insensitive (see ref below),
this uses the `Headers` implementation which we use within
`apollo-server-core` to manage and manipulate headers.

The `node-fetch` headers implementation takes care to store the headers in a
case-insensitive manner internally:

https://github.com/bitinn/node-fetch/blob/bf8b4e8db350ec76dbb9236620f774fcc21b8c12/src/headers.js#L267-L270

...which should hopefully simplify the logic and make things behave more
reliably if someone chooses to use a peculiar case for the header.

As to Amazon's own difference in headers, it appears to be that it's a
difference between their Application-Load Balancer behavior and the way that
their AWS API Gateway functionality sends their `events` — the former being
the case of the original request and the latter being lower-cased
behind-the-scenes.

Ref: https://tools.ietf.org/html/rfc7230#section-3.2 (RFC)
  • Loading branch information
abernix committed May 15, 2019
1 parent 7fb5a29 commit fde1d36
Showing 1 changed file with 45 additions and 27 deletions.
72 changes: 45 additions & 27 deletions packages/apollo-server-lambda/src/ApolloServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
APIGatewayProxyCallback,
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context as LambdaContext,
} from 'aws-lambda';
import { ApolloServerBase } from 'apollo-server-core';
Expand All @@ -12,6 +11,7 @@ import {
} from '@apollographql/graphql-playground-html';

import { graphqlLambda } from './lambdaApollo';
import { Headers } from 'apollo-server-env';

export interface CreateHandlerOptions {
cors?: {
Expand Down Expand Up @@ -54,42 +54,47 @@ export class ApolloServer extends ApolloServerBase {
// the GraphQLServerOptions function which is called before each request.
const promiseWillStart = this.willStart();

const corsHeaders: APIGatewayProxyResult['headers'] = {};
const corsHeaders = new Headers();

if (cors) {
if (cors.methods) {
if (typeof cors.methods === 'string') {
corsHeaders['Access-Control-Allow-Methods'] = cors.methods;
corsHeaders.set('access-control-allow-methods', cors.methods);
} else if (Array.isArray(cors.methods)) {
corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(',');
corsHeaders.set(
'access-control-allow-methods',
cors.methods.join(','),
);
}
}

if (cors.allowedHeaders) {
if (typeof cors.allowedHeaders === 'string') {
corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders;
corsHeaders.set('access-control-allow-headers', cors.allowedHeaders);
} else if (Array.isArray(cors.allowedHeaders)) {
corsHeaders[
'Access-Control-Allow-Headers'
] = cors.allowedHeaders.join(',');
corsHeaders.set(
'access-control-allow-headers',
cors.allowedHeaders.join(','),
);
}
}

if (cors.exposedHeaders) {
if (typeof cors.exposedHeaders === 'string') {
corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders;
corsHeaders.set('access-control-expose-headers', cors.exposedHeaders);
} else if (Array.isArray(cors.exposedHeaders)) {
corsHeaders[
'Access-Control-Expose-Headers'
] = cors.exposedHeaders.join(',');
corsHeaders.set(
'access-control-expose-headers',
cors.exposedHeaders.join(','),
);
}
}

if (cors.credentials) {
corsHeaders['Access-Control-Allow-Credentials'] = 'true';
corsHeaders.set('access-control-allow-credentials', 'true');
}
if (cors.maxAge) {
corsHeaders['Access-Control-Max-Age'] = cors.maxAge;
if (typeof cors.maxAge === 'number') {
corsHeaders.set('access-control-max-age', cors.maxAge.toString());
}
}

Expand All @@ -98,23 +103,34 @@ export class ApolloServer extends ApolloServerBase {
context: LambdaContext,
callback: APIGatewayProxyCallback,
) => {
// We re-load the headers into a Fetch API-compatible `Headers`
// interface within `graphqlLambda`, but we still need to respect the
// case-insensitivity within this logic here, so we'll need to do it
// twice since it's not accessible to us otherwise, right now.
const eventHeaders = new Headers(event.headers);

if (cors && cors.origin) {
const requestOrigin = eventHeaders.get('origin');
if (typeof cors.origin === 'string') {
corsHeaders['Access-Control-Allow-Origin'] = cors.origin;
corsHeaders.set('access-control-allow-origin', cors.origin);
} else if (
typeof cors.origin === 'boolean' ||
(Array.isArray(cors.origin) &&
cors.origin.includes(
event.headers['Origin'] || event.headers['origin'],
))
requestOrigin &&
(typeof cors.origin === 'boolean' ||
(Array.isArray(cors.origin) &&
requestOrigin &&
cors.origin.includes(requestOrigin)))
) {
corsHeaders['Access-Control-Allow-Origin'] =
event.headers['Origin'] || event.headers['origin'];
corsHeaders.set('access-control-allow-origin', requestOrigin);
}

if (!cors.allowedHeaders) {
corsHeaders['Access-Control-Allow-Headers'] =
event.headers['Access-Control-Request-Headers'];
const requestAccessControlRequestHeaders = eventHeaders.get(
'access-control-request-headers',
);
if (!cors.allowedHeaders && requestAccessControlRequestHeaders) {
corsHeaders.set(
'access-control-allow-headers',
requestAccessControlRequestHeaders,
);
}
}

Expand All @@ -123,7 +139,9 @@ export class ApolloServer extends ApolloServerBase {
return callback(null, {
body: '',
statusCode: 204,
headers: corsHeaders,
headers: {
...corsHeaders,
},
});
}

Expand Down

0 comments on commit fde1d36

Please sign in to comment.