11
votes

The core question here is: "how do I allow custom headers in a CORS GET request that is handled with the Serverless framework?". If you know the answer to that, pass Go, collect $200 and please answer that question. If it's not a question with a straight answer, here are the details:

I am writing an app using the Serverless framework on AWS Lambda (the API is managed through AWS API Gateway. Frankly, I'm not entirely sure what that means or what benefit that provides me but that's what Serverless automatically configured for me). I am attempting to create an open API which requires CORS to be enabled. I am using the Lambda Proxy integration. I have followed the practices found here. They have brought me partial success. My app currently has CORS enabled if I do not include my custom headers. However, it still does not work with custom headers.

When I send the following request to my API:

var data = null;

var xhr = new XMLHttpRequest();
xhr.withCredentials = false;

xhr.addEventListener("readystatechange", function () {
  if (this.readyState === 4) {
    console.log(this.responseText);
  }
});

xhr.open("GET", "https://api.spongebobify.com/");
xhr.setRequestHeader("text", "hey");


xhr.send(data);

... I get this error:

Failed to load https://api.spongebobify.com/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://forum.serverless.com' is therefore not allowed access.

This error message is confirmed if I check the "response headers" using Chrome dev tools: there is no Access-Control-Allow-Origin in the response headers.

However, if I send the same request with the setRequestHeader() commented out, it works perfectly (yes, I know it returns a 403 error: that is intentional behavior).

Here's what I think is happening. My service has two potential CORS problems: domain related (a request not coming from the origin domain) and custom header related (a header not safe-listed by the CORS spec, more here). Somehow, the Serverless framework trips up on the second issue which causes it not even get to the point where it issues the appropriate headers to allow all ("*") domains.

Here is my serverless.yml config file:

# serverless.yml

service: spongebobify

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: us-east-1

functions:
  app:
    handler: handler.endpoint
    events:
      - http: GET /
        cors:
            origin: '*'
            headers:
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
              - X-Amz-User-Agent
              - Startlower
              - Text
              - Access-Control-Allow-Headers
              - Access-Control-Allow-Origin
            allowCredentials: false

and here is the function that I am trying to run. You can see my many attempts to set the headers properly. I'm 60% convinced that a fix will come via the serverless.yml file at this point.

"use strict";

const spongebobify = require("spongebobify");

module.exports.endpoint = (event, context, callback) => {
  let startLower = event.headers.startlower === "false" ? false : true;
  try {
    const response = {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*", // Required for CORS support to work
        "Access-Control-Allow-Headers": "content-type,origin,text,startlower",
        "Access-Control-Allow-Methods": "GET, OPTIONS",
        "content-type": "text/plain",
        "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
      },
      body: spongebobify(event.headers.text, startLower)
    };
    callback(null, response);
  } catch (err) {
    console.log(err);
    const response = {
      statusCode: 403,
      headers: {
        "Access-Control-Allow-Origin": "*", // Required for CORS support to work
        "Access-Control-Allow-Headers": "content-type,origin,X-text,startlower",
        "Access-Control-Allow-Methods": "GET, OPTIONS",
        "content-type": "text/plain",
        "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
      },
      body: "Malformed request."
    };
    callback(null, response);
  }
};

You can replicate my problem my running the above XMLHttpRequest in the dev console on the following sites:

  1. api.spongebobify.com with the custom header enabled or disabled. It will work perfectly in both cases (because it won't be cross origin).
  2. Any site that doesn't have a properly configured CSP with the custom header enabled. The OPTIONS request will fail and it will accurately report that there is no Access-Control-Allow-Origin header
  3. Any site that doesn't have a properly configured CSP without the custom header enabled. The OPTIONS request will pass (which you'll know because Chrome will never tell you that it happened) and you will see the Access-Control-Allow-Origin in the response header. You will also see the response "Malformed request.".
1
I don’t know anything about Serverless but from looking at the serverless.yml file in the question, I see what looks like config for handling GET requests but not anything for handling OPTIONS requests. To get the cors config, don’t you need to explicit handling for OPTIONS requests to that file? - sideshowbarker
I thought that at first too - nothing in the Serverless documentation about it and it seems to handle the OPTIONS just fine without the custom headers so I doubt that's it. - Ben Cooper
The thing is, without the custom header added to the request, you’re browser isn’t doing an OPTIONS request. That xhr.setRequestHeader("text", "hey") code is what causes the browser to send the OPTIONS request. When you remove that, the browser just sends the GET request directly, without first doing any OPTIONS preflight. - sideshowbarker
Wouldn’t it have to send an OPTIONS request to verify that the domain is open to CORS requests in the first place? - Ben Cooper
No — see developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests. Your no-added-headers GET request is a “simple request” that doesn’t require a preflight - sideshowbarker

1 Answers

16
votes

I think the issue is that you're mixing the short form of the HTTP event (- http: GET /) with the long form that adds additional options.

Try using this:

functions:
  app:
    handler: handler.endpoint
    events:
      - http: 
          method: GET 
          path: /
          cors:
            origin: '*'
            headers:
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
              - X-Amz-User-Agent
              - Startlower
              - Text
              - Access-Control-Allow-Headers
              - Access-Control-Allow-Origin
            allowCredentials: false

The main changes are:

1) Adding method and path keys on the http event object, and

2) Indenting the cors object another level. It was previously at the top level of the http event.

Let me know if this helps :)