3

I have a sam file trying to build an API Gateway to a lambda function. I am following the latest AWS documentation for configuring cors. This documentation is quoted here:

enter image description here

As you may have guessed, that isn't working for me. If I use "http://localhost:3000" or a variation of that as the "AllowOrigin" property, then I get the following error:

Access to XMLHttpRequest at 'xxx' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Checking the API gateway, that property is indeed present. Although it seems to be only present for the options and not for the "ANY" section.

enter image description here

If I change that to be AllowOrigin: "'*'", I get the following error:

Access to XMLHttpRequest at 'xxxx' from origin 'http://localhost:3000' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.

In addition to this, there is a separate "Stage" stage created which I don't understand and I am not sure that the proxy needs to be there or where it is created in the SAM file.

Here is my yaml file for sam.

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Storygraf backend API

Globals:
  Function:
    Timeout: 3

Resources:
  ExpressApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      Cors:
        AllowOrigin: "'*'"
        AllowMethods: "'POST, GET, PUT, DELETE'"
        AllowHeaders: "'X-Forwarded-For, Content-Type'"

  ExpressLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: lambda.handler
      MemorySize: 512
      Runtime: nodejs14.x
      Timeout: 30
      Events:
        ProxyApiRoot:
          Type: Api
          Properties:
            RestApiId: !Ref ExpressApi
            Path: /
            Method: ANY
        ProxyApiGreedy:
          Type: Api
          Properties:
            RestApiId: !Ref ExpressApi
            Path: /{proxy+}
            Method: ANY

Update

It looks like the test works when going through the proxy, but nothing is available when using the non proxy GET command.

enter image description here

enter image description here

Update

I discovered that the package process is changing the quotes, but this doesn't seem to be the problem.

halfer
  • 19,824
  • 17
  • 99
  • 186
Joshua Foxworth
  • 1,236
  • 1
  • 22
  • 50
  • I have struggled with this problem in the past and a few things I realised is (1) you can get a CORS error for non CORS reasons. You spend ages trying to sort out CORS and there is in fact another problem. (2) Always remember to deploy your API. Sometimes you make changes and forget to deploy. If you deploy in your cloudformation you won't have to worry about this. (3) The API Actions button allows you to enable CORS. Sometimes this helps and then you can compare to your cloudformation template where you differ. (4) make sure the stage name is in your path when you call the API. – RodP Aug 22 '22 at 17:34
  • Unfortunately, I have tried all of those things. There are dozens of docs trying to explain how to set this up in AWS and none are the same. In the end, it's a little absurd that you can follow a doc on something so simple, get a different result and an error and have no mechanism to fix it or understand what is happening. AWS is kinda terrible when compared to Heroku and google cloud. – Joshua Foxworth Aug 22 '22 at 17:37
  • One last thing to try, did you test in the API? Does the API work without making a call to it? – RodP Aug 22 '22 at 17:43
  • It works if I hit the end point in insomnia or postman. Is that what you mean? – Joshua Foxworth Aug 22 '22 at 17:57
  • There is a test button in the API. When you click on any of your methods (GET, POST) you will see the request, response and integration boxes and a test box on the left. You can specify headers, data, etc in there. If you can get it working in test then you narrow down your issue. – RodP Aug 22 '22 at 18:03
  • 1
    Interesting. I added some images. The test works when I use the proxy, but not on the regular end point. The proxy really confuses me. Why is it there? How did it get added? – Joshua Foxworth Aug 22 '22 at 18:28
  • `www.example.com` is not a valid Web origin. Did you mean `https://www.example.com`? – jub0bs Aug 22 '22 at 20:40
  • Sorry can't comment on the proxy. I avoid proxy and tend to use lambda function integration with mapping templates. It gives me the best control. I can share some CDK code with you but its quite different to cloudformation templates. That said it all gets converted to cloudformation in the end so the basics are the same.. – RodP Aug 22 '22 at 21:14
  • @jub0bs That is actually a screen shot from the AWS documentation. – Joshua Foxworth Aug 22 '22 at 23:38
  • OK so looking through this code and your other question - can you try just using single quotes for the CORS and lets focus on making it work using '*' which should work :) if that doesn't work, still update the question with the single quote variant not a combo. – Ermiya Eskandary Aug 28 '22 at 05:21
  • In AWS documentation it says, "After CORS is enabled on the GET method, an OPTIONS method is added to the resource. The 200 response of the OPTIONS method returns three Access-Control-Allow-* headers. In addition, the actual (GET) method is also configured by default to return the Access-Control-Allow-Origin header in its 200 response as well. For other types of responses, you will need to manually configure them to return Access-Control-Allow-Origin' header with '*' or specific origins, if you do not want to return the Cross-origin access error." – Todd Walton Nov 04 '22 at 19:50
  • Check that you're setting the headers on the methods you need, and also on the OPTIONS method. You're setting it in your API definition, but it may for some reason not set it on all the methods defined. – Todd Walton Nov 04 '22 at 19:51

2 Answers2

0

From your logs your access-control-allow-origin header returned from your GET test returns an array with ['*','*']. It should not be an array and should just be '*'.

Edit enter image description here

Derrops
  • 7,651
  • 5
  • 30
  • 60
  • It's not an array - it's `AllowOrigin: "'*'"` – Ermiya Eskandary Aug 29 '22 at 12:32
  • Are you sure? That doesn't look write. – Derrops Aug 29 '22 at 12:39
  • It doesn't, yes - but the SAM YAML file OP is using specifies only one element and not an array. It may be a bug with the SAM framework producing the resulting CloudFormation script - check this one: https://stackoverflow.com/questions/73490786/sam-package-is-adding-quotes-to-my-yaml-file – Ermiya Eskandary Aug 29 '22 at 12:42
  • Yeah that looks like the issue, that is very very annoying.... These tools that are supposed to make your life ez don't do that sometimes. – Derrops Aug 29 '22 at 12:46
0

This is the template I use, which has a few more things defined. Also I've got an API key there, which you can easily remove.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Storygraf backend API

Resources:

  ApiGatewayApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: ExpressApi
      StageName: Prod
      Auth:
        UsagePlan: 
          CreateUsagePlan: PER_API
          UsagePlanName: "Basic Usage Plan"
          Throttle:
            BurstLimit: 5
            RateLimit: 5
        ResourcePolicy:
          CustomStatements: [
            {
              "Effect": "Allow",
              "Principal": "*",
              "Action": "execute-api:Invoke",
              "Resource": "execute-api:/Prod/POST/express"
            },
            {
              "Effect": "Allow",
              "Principal": "*",
              "Action": "execute-api:Invoke",
              "Resource": "execute-api:/Prod/OPTIONS/express"
            }]
      DefinitionBody:
        swagger: '2.0'
        info:
          title: 
            Ref: AWS::StackName
        produces:
          - application/json
        x-amazon-apigateway-api-key-source: "HEADER"
        paths:
          '/express:
            get:
              security:
                - ApiKeyAuth: []
              x-amazon-apigateway-auth:
                type : "NONE"
              x-amazon-apigateway-api-key-source: "HEADER"
              produces:
                - application/json
              responses:
                '200':
                  statusCode: 200
                  description: 200 response
                  schema:
                    $ref: "#/definitions/Empty"
                  headers:
                    Content-Type:
                      type: string
                    Access-Control-Allow-Origin:
                      type: string
              x-amazon-apigateway-integration:
                uri: 
                  Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ExpressLambdaFunction.Arn}/invocations"
                httpMethod: POST //This must always be POST! Even if your method is a GET / PUT etc...
                type: AWS_PROXY
                responses:
                  default:
                    statusCode: 200
                    schema:
                      $ref: "#/definitions/Empty"
                    responseParameters:
                      method.response.header.Access-Control-Allow-Origin: "'*'"
                      method.response.header.Content-Type: integration.response.header.content-type
                passthroughBehavior: when_no_match
            options:
              x-amazon-apigateway-auth:
                type : "NONE"
              consumes:
                - application/json
              produces:
                - application/json
              responses:
                '200':
                  description: 200 response
                  schema:
                    $ref: "#/definitions/Empty"
                  headers:
                    Access-Control-Allow-Origin:
                      type: string
                    Access-Control-Allow-Methods:
                      type: string
                    Access-Control-Allow-Headers:
                      type: string
              x-amazon-apigateway-integration:
                responses:
                  default:
                    statusCode: 200
                    responseParameters:
                      method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'"
                      method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                      method.response.header.Access-Control-Allow-Origin: "'*'"
                passthroughBehavior: when_no_match
                requestTemplates:
                  application/json: "{\"statusCode\": 200}"
                type: mock
        securityDefinitions:
            ApiKeyAuth:
              type: apiKey
              in: header
              name: x-api-key
        definitions:
            Empty:
              type: object
              title: Empty Schema

  ExpressLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 100
      CodeUri: <relative path to your lambda function>
      Handler: app.lambdaHandler //Assuming method lambdahandler is in app.js
      Runtime: nodejs14.x
      Role: !GetAtt LambdaExecutionRole.Arn
      Events:
        ProxyApiRoot:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGatewayApi
            Path: /express
            Method: get
    
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: "Allow"
            Principal: 
              Service: 
                - "lambda.amazonaws.com"
            Action: 
              - "sts:AssumeRole"
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - <Any other required policy arns>

Make sure in your lambda response your headers match those defined in the cloud formation OPTIONS response, for the template I've given the response from the lambdaHandler function may look like this (if using js):

return {
  'statusCode': 200,
  'headers': {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*"
  },
  'body': JSON.stringify({
    "Message": "Successfully did something"
  })
}

Hope this is somewhat helpful.

Mark
  • 136
  • 9
  • Thank you, this was the step I was missing! `Make sure in your lambda response your headers match those defined in the cloud formation OPTIONS response, for the template I've given the response from the lambdaHandler function may look like this (if using js)` – Chris Jan 06 '23 at 12:18