CORS issues with Serverless Lambda and API Gateway

Josh picture Josh · Mar 18, 2018 · Viewed 7.8k times · Source

Solved

The below issue was simply caused by the body property of the response object constructed in my Lambda. I was forgetting to stringify the data, returning body: data instead of body: JSON.stringify(data). This problem with the response appeared to trigger an error with API Gateway which caused the request failures with some rather confusing error messages.

Problem

I'm working on a ecommerce site using React, Serverless and the Stripe API. My front-end React app is making a GET request using Axios to my Lambda function which has been exposed via API Gateway. The Lambda function in turn queries the Stripe API and returns the Stripe response data to my React app. However, I am experiencing CORS issues as my React app tries to call the Lambda, it receives the following error:

Failed to load: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. The response had HTTP status code 502.

Querying the Lambda endpoint in Insomnia returns a 502 response with { "message": "internal server error" }. But executing the serverless invoke command from the Lambda function from the terminal successfully returns the Stripe data.

I am enabling cors for the function in my serverless.yml file and including 'Access-Control-Allow-Origin': '*' in my Lambda code response as advised in this Serverless blog post, I have also attempted adding a various combinations of the following headers to my Lambda response and my Axios request based on suggestions found on this issue on Axios and this issue on Serverless. I've deleted and redeployed the service multiple times and

Lambda response headers

headers: {
      'Access-Control-Expose-Headers': 'Access-Control-Allow-Origin',
      'Access-Control-Allow-Credentials': true,
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },

React/Axios config

  {
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    crossDomain: true
  }

Currently my code is as follows:

React app

import Axios from 'axios';
import { GET_PRODUCTS_URL } from '../config';

export default () => {
  return Axios
  .get(GET_PRODUCT_URL, {
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    crossDomain: true
  })
  .then(({ data }) => data)
  .catch(console.log);
}

Lambda

import Stripe from 'stripe';
const apiKey = process.env.STRIPE_SECRET_KEY;

const stripe = Stripe(apiKey);

module.exports.handler = (event, context, callback) => {
  return stripe.products
    .list()
    .then(({ data }) => {
      const response = {
        statusCode: 200,
        headers: {
          'Access-Control-Expose-Headers': 'Access-Control-Allow-Origin',
          'Access-Control-Allow-Credentials': true,
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*',
        },
        body: data,
      };
      callback(null, response);
    })
    .catch(console.log);
}

Serverless.yml

service: srd

provider:
  name: aws
  runtime: nodejs6.10
  stage: ${opt:stage, self:custom.defaultStage}
  region: eu-west-1
  memorySize: 256
  timeout: 6
  versionFunctions: true

plugins:
  - serverless-plugin-optimize
  - serverless-plugin-scripts

package:
  individually: true

functions:
  get-products:
    handler: lambda/get-products.handler
    name: srd-get-products-${self:provider.stage}
    description: 'get srd products from stripe'
    environment:
      STRIPE_PUBLISHABLE_KEY: ${self:custom.stripe_publishable_key.${self:provider.stage}}
      STRIPE_SECRET_KEY: ${self:custom.stripe_secret_key.${self:provider.stage}}
    events:
      - http:
          path: products
          method: get
          cors: true

custom:
  defaultStage: dev
  stripe_publishable_key:
    dev: ${file(env.yml):STRIPE_DEV_PUBLISHABLE_KEY}
    live: ${file(env.yml):STRIPE_LIVE_PUBLISHABLE_KEY}
  stripe_secret_key:
    dev: ${file(env.yml):STRIPE_DEV_SECRET_KEY}
    live: ${file(env.yml):STRIPE_LIVE_SECRET_KEY}

I've been at this for hours, any insights much appreciated.

Edit/Additional Making an options request from the CLI returns the following:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 0
Connection: keep-alive
Date: Mon, 19 Mar 2018 06:52:12 GMT
x-amzn-RequestId: 0d10c9d1-2b42-11e8-b270-a723e367048e
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
Access-Control-Allow-Methods: OPTIONS,GET
Access-Control-Allow-Credentials: false
X-Cache: Miss from cloudfront
Via: 1.1 e40b14deb4a844594f746b751f632677.cloudfront.net (CloudFront)
X-Amz-Cf-Id: eMvu3Ke7m7GNFCFgIOGVJmoObFwYMeEt4o8AByoipfMn1nXIi9Vo0g==

Answer

Lansana Camara picture Lansana Camara · Mar 18, 2018

The HTTP response code in the CORS error message says 502. This means the server you are requesting is unavailable.

In an event of a 502, the browser cannot make successful OPTIONS requests, so even if your CORS setup in the server is correct, you will still get a CORS error since it was not resolved properly.

Look at your server and make sure it is running as it should. Once the 502 error is fixed, try again and you should be good to go. Try making a manual OPTIONS request, similar to that of what the browser would make, and see if you get the same error.