apollo-server-integrations / apollo-server-integration-aws-lambda

An integration to use AWS Lambda as a hosting service with Apollo Server
MIT License
46 stars 9 forks source link

CORS Preflight requests fail with 400 status #96

Closed ChrisW-B closed 1 year ago

ChrisW-B commented 1 year ago

This may be a better question for a support channel, so please redirect me if there's a better place to ask!

I've recently updated from apollo-server-core@3.12.0 and apollo-server-lambda@3.12.0 to @apollo/server@4.7.0 and @as-integrations/aws-lambda@2.0.1. I've been running this server on Netlify. This worked pretty well with v3.12.0, but after updating both Chrome and Firefox fail with a 400 error on the OPTIONS preflight request, meaning no data loads. Using the API directly in the browser with Apollo Studio works perfectly, and data loads in Safari fine as well.

My setup code for the server looks like the following:

import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
import { handlers, startServerAndCreateLambdaHandler } from '@as-integrations/aws-lambda';

import schema from './db/graphql';

const HEADERS = {
  'access-control-allow-headers': 'content-type',
  'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
  'access-control-allow-origin': '*',
};

const server = new ApolloServer({
  schema,
  introspection: true,
  plugins: [ApolloServerPluginLandingPageLocalDefault()],
});

const apolloHandler = handlers.createAPIGatewayProxyEventRequestHandler();

export const handler = startServerAndCreateLambdaHandler(server, apolloHandler, {
  middleware: [
    async () => {
      return async (result) => {
        result.headers = { ...result.headers, ...HEADERS };
      };
    },
  ],
});

Am I missing some extra CORS setup?

Thanks for any help!

trevor-scheer commented 1 year ago

Shouldn't OPTIONS be in your list of allowed methods? I'm not a lambda expert, is there some configuration at the lambda level you need to make in order to handle OPTIONS requests? (@BlenderDude will know better than I)

@BlenderDude we should probably get a CORS example up in the README here.

ChrisW-B commented 1 year ago

haha my bad, I posted an old example- I've tried with '*' as well as 'GET, POST, OPTIONS' since, but let me retry just in case I changed something else since then

BlenderDude commented 1 year ago

@trevor-scheer given how popular the CORS request is, I intent on providing a built-in middleware that handles it, and will be adding that to the README.

The issue here is that Chrome does not allow for the * origin wildcard. Most middleware implementations get around this by substituting * with req.origin. For API gateway v1, you'll need to figure out what headers and environment are exposed from Netlify. If there is an Origin header available, I recommend using req.headers.Origin. This isn't always available though. In my experience, a more robust and portable solution is to utilize req.headers.Host. Methods can be safely updated to *. The updated handler would look like the following:

import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
import { handlers, startServerAndCreateLambdaHandler } from '@as-integrations/aws-lambda';

import schema from './db/graphql';

const server = new ApolloServer({
  schema,
  introspection: true,
  plugins: [ApolloServerPluginLandingPageLocalDefault()],
});

const apolloHandler = handlers.createAPIGatewayProxyEventRequestHandler();

export const handler = startServerAndCreateLambdaHandler(server, apolloHandler, {
  middleware: [
    async (event) => {
      // Below is an untested example, but should get you on the right track
      let origin = event.headers.Origin;
      if (origin === undefined) {
        const host = event.headers.Host;
        // Assume localhost is insecure, and everything else is using https
        const isSecure = !/^localhost:\d{1,5}$/.test(host);
        origin = `${isSecure ? 'https' : 'http'}://${host}`;
      }
      return async (result) => {
        result.headers = {
          ...result.headers,
          'access-control-allow-headers': 'content-type',
          'access-control-allow-methods': '*',
          'access-control-allow-origin': origin,
        };
      };
    },
  ],
});
ChrisW-B commented 1 year ago

@BlenderDude thank you! I'll look into what Netlify allows and give that a try

ChrisW-B commented 1 year ago

Sadly no luck, I rewrote the middleware to

 async (event) => {
      let origin = event.headers.Origin;
      const host = event.headers.Host;
      if (!origin && host) {
        const isSecure = /^localhost:\d{1,5}$/.test(host);
        origin = `${isSecure ? 'https' : 'http'}://${host}`;
      } else if (!origin) {
        origin = '*';
      }
      return async (result) => {
        result.headers = {
          ...result.headers,
          'Access-Control-Allow-Headers': '*',
          'Access-Control-Allow-Methods': '*',
          'Access-Control-Allow-Origin': origin ?? '*',
        };
      };
    },

(I modified the Allowed Headers because they were breaking the apollo studio setup)

and it looks like netlify isn't passing the origin or host, as I get an * for the value in the end:

Screenshot 2023-04-24 at 3 56 24 PM

I'll spend some more time looking through netlify's docs later; but for now I'm going to roll this release back and keep an eye out for your built in middleware.

Thanks again for the help!

rdok commented 1 year ago

Partial fix:

...
 return async (result: APIGatewayProxyResult) => {
    if (event.httpMethod.toUpperCase() === "OPTIONS") {
      result.statusCode = 200;
      result.body = "";
    }
...

Not a proper fix as apollo server still process the request.

Inspiration https://stackoverflow.com/a/63888972/2790481