aws-sam-cli: Problem with OPTIONS requests
Description:
Trying to access my API from a test app in my browser and the OPTIONS
request is being handled by SAM CLI API Gateway but it’s not returning the configuration specified in my template file.
The weird part is that if I try to access a different endpoint from the same template it executes the lambda function (as expected since that’s what SAM Local did).
Additional environment details (Ex: Windows, Mac, Amazon Linux etc) Mac
Output of sam --version
:
SAM CLI, version 0.3.0
Optional Debug logs:
> export AWS_SDK_LOAD_CONFIG=true && sam local start-api --docker-network myblueprint "--debug-port" "5858"
2018-05-10 16:23:38 Mounting HealthCheckFunction at http://127.0.0.1:3000/healthcheck [OPTIONS, GET]
2018-05-10 16:23:38 Mounting EnsureUserFunction at http://127.0.0.1:3000/ensure_user [OPTIONS, PUT]
2018-05-10 16:23:38 Mounting GraphQLServerFunction at http://127.0.0.1:3000/graphql [GET, POST, OPTIONS]
2018-05-10 16:23:38 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2018-05-10 16:23:38 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2018-05-10 16:24:07 127.0.0.1 - - [10/May/2018 16:24:07] "OPTIONS /graphql HTTP/1.1" 200 -
2018-05-10 16:24:45 Invoking dist/healthcheck.handler (nodejs8.10)
2018-05-10 16:24:45 Found credentials in shared credentials file: ~/.aws/credentials
Fetching lambci/lambda:nodejs8.10 Docker container image......
2018-05-10 16:24:49 Mounting /Users/stefan/Projects/blueprint-api/packages/api as /var/task:ro inside runtime container
Debugger listening on ws://0.0.0.0:5858/698de0ac-e3af-4f27-b5dd-bf3caf4cbc58
For help see https://nodejs.org/en/docs/inspector
Debugger attached.
Debugger listening on ws://0.0.0.0:5858/698de0ac-e3af-4f27-b5dd-bf3caf4cbc58
For help see https://nodejs.org/en/docs/inspector
START RequestId: 2ee985d7-416b-182e-ec4a-79af497b0cc2 Version: $LATEST
2018-05-10T04:25:06.756Z 2ee985d7-416b-182e-ec4a-79af497b0cc2 ---TRIMMED ERROR---
END RequestId: 2ee985d7-416b-182e-ec4a-79af497b0cc2
REPORT RequestId: 2ee985d7-416b-182e-ec4a-79af497b0cc2 Duration: 2838.28 ms Billed Duration: 2900 ms Memory Size: 1024 MB Max Memory Used: 52 MB
2018-05-10 16:25:03 No Content-Type given. Defaulting to 'application/json'.
2018-05-10 16:25:03 127.0.0.1 - - [10/May/2018 16:25:03] "OPTIONS /healthcheck HTTP/1.1" 503 -
Template file for reference
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: MyBlueprint API
Parameters:
Environment:
Type: String
AllowedValues:
- DEVELOPMENT
- STAGING
- PRODUCTION
# DEV SHOULD NEVER BE RUN OUTSIDE OF SAM LOCAL
Mappings:
Config:
'DEVELOPMENT':
EnvironmentLowerCase: 'development'
HostedZoneName: 'localhost'
HostedZoneId: 'NONE'
S3FilesBucket: 'blah'
'STAGING':
EnvironmentLowerCase: 'staging'
HostedZoneName: '<REDACTED>'
HostedZoneId: '<REDACTED>'
S3FilesBucket: '<REDACTED>'
'PRODUCTION':
EnvironmentLowerCase: 'production'
HostedZoneName: '<REDACTED>'
HostedZoneId: '<REDACTED>'
S3FilesBucket: '<REDACTED>'
Conditions:
CreateS3FileProcessingResources:
!Equals [!Ref 'AWS::Region', 'ap-southeast-2']
Globals:
Function:
Runtime: 'nodejs8.10'
MemorySize: 1024
Timeout: 5
Tracing: PassThrough
Environment:
Variables:
CODE_ENVIRONMENT: !Ref Environment
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: 'sts:AssumeRole'
Effect: Allow
Principal:
Service: 'lambda.amazonaws.com'
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
- 'arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess'
Policies:
- PolicyName: s3
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 's3:*'
Effect: Allow
Resource: '*'
- PolicyName: parameters
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'ssm:GetParameter'
- 'ssm:GetParameters'
- 'ssm:GetParametersByPath'
Resource:
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${Environment}/API/*'
- Effect: Allow
Action:
- 'kms:Decrypt'
Resource:
- !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*'
MyBlueprintApi:
Type: 'AWS::Serverless::Api'
Properties:
StageName: 'prod'
EndpointConfiguration: 'REGIONAL'
Cors:
AllowMethods: "'GET,OPTIONS,POST,PUT'"
AllowHeaders: "'Authorization,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
AllowOrigin: "'*'"
MaxAge: "'86400'"
DefinitionBody:
swagger: '2.0'
info:
version: '0.0.1'
title:
'Fn::Sub':
- 'myblueprint-${EnvironmentLowerCase}'
- EnvironmentLowerCase: !FindInMap [Config, !Ref Environment, EnvironmentLowerCase]
basePath: '/prod'
schemes:
- 'https'
paths:
'/graphql':
get:
consumes:
- 'application/json'
produces:
- 'application/json'
responses:
'200':
description: '200 response'
schema:
$ref: '#/definitions/Empty'
headers:
Access-Control-Allow-Origin:
type: 'string'
x-amazon-apigateway-integration:
responses:
default:
statusCode: '200'
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GraphQLServerFunction.Arn}/invocations'
passthroughBehavior: 'when_no_templates'
httpMethod: 'POST'
contentHandling: 'CONVERT_TO_TEXT'
type: 'aws_proxy'
post:
consumes:
- 'application/json'
produces:
- 'application/json'
responses:
'200':
description: '200 response'
schema:
$ref: '#/definitions/Empty'
headers:
Access-Control-Allow-Origin:
type: 'string'
x-amazon-apigateway-integration:
responses:
default:
statusCode: '200'
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GraphQLServerFunction.Arn}/invocations'
passthroughBehavior: 'when_no_templates'
httpMethod: 'POST'
contentHandling: 'CONVERT_TO_TEXT'
type: 'aws_proxy'
options:
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,OPTIONS,POST'"
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: "'*'"
requestTemplates:
application/json: '{"statusCode": 200}'
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GraphQLServerFunction.Arn}/invocations'
passthroughBehavior: 'when_no_match'
type: 'mock'
'/ensure_user':
put:
consumes:
- 'application/json'
produces:
- 'application/json'
responses:
'200':
description: '200 response'
schema:
$ref: '#/definitions/Empty'
headers:
Access-Control-Allow-Origin:
type: 'string'
x-amazon-apigateway-integration:
responses:
default:
statusCode: '200'
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EnsureUserFunction.Arn}/invocations'
passthroughBehavior: 'when_no_templates'
httpMethod: 'POST'
contentHandling: 'CONVERT_TO_TEXT'
type: 'aws_proxy'
options:
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: "'PUT,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: "'*'"
requestTemplates:
application/json: '{"statusCode": 200}'
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EnsureUserFunction.Arn}/invocations'
passthroughBehavior: 'when_no_match'
type: 'mock'
'/healthcheck':
get:
consumes:
- 'application/json'
produces:
- 'application/json'
responses:
'200':
description: '200 response'
schema:
$ref: '#/definitions/Empty'
headers:
Access-Control-Allow-Origin:
type: 'string'
x-amazon-apigateway-integration:
responses:
default:
statusCode: '200'
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HealthCheckFunction.Arn}/invocations'
passthroughBehavior: 'when_no_templates'
httpMethod: 'POST'
contentHandling: 'CONVERT_TO_TEXT'
type: 'aws_proxy'
options:
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,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: "'*'"
requestTemplates:
application/json: '{"statusCode": 200}'
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HealthCheckFunction.Arn}/invocations'
passthroughBehavior: 'when_no_match'
type: 'mock'
definitions:
Empty:
type: 'object'
title: 'Empty Schema'
GraphQLServerFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: 'dist/graphql.handler'
CodeUri: './packages/api/'
Role: !GetAtt LambdaExecutionRole.Arn
MemorySize: 1536
Timeout: 30
Events:
GetResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/graphql'
Method: get
OptionsResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/graphql'
Method: options
PostResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/graphql'
Method: post
EnsureUserFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: 'dist/ensure_user.handler'
CodeUri: './packages/api/'
Role: !GetAtt LambdaExecutionRole.Arn
Events:
PutResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/ensure_user'
Method: put
OptionsResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/ensure_user'
Method: options
S3FileProcessingFunction:
Type: 'AWS::Serverless::Function'
Condition: 'CreateS3FileProcessingResources'
Properties:
Handler: 'dist/s3files.handler'
CodeUri: './packages/api/'
Role: !GetAtt LambdaExecutionRole.Arn
Timeout: 30
# Cant use this because the bucket must be created in the same template
# Events:
# ObjectCreated:
# Type: S3
# Properties:
# Bucket: !FindInMap [Config, !Ref Environment, S3FilesBucket]
# Events: s3:ObjectCreated:*
HealthCheckFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: 'dist/healthcheck.handler'
CodeUri: './packages/api/'
Role: !GetAtt LambdaExecutionRole.Arn
Events:
GetResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/healthcheck'
Method: get
OptionsResource:
Type: Api
Properties:
RestApiId: !Ref MyBlueprintApi
Path: '/healthcheck'
Method: options
# TODO: update this to use DNS validation when CF supports it
ApiCertificate:
Type: 'AWS::CertificateManager::Certificate'
Properties:
DomainName:
'Fn::Sub':
- 'api.${HostedZoneName}'
- HostedZoneName: !FindInMap [Config, !Ref Environment, HostedZoneName]
DomainValidationOptions:
- DomainName:
'Fn::Sub':
- 'api.${HostedZoneName}'
- HostedZoneName: !FindInMap [Config, !Ref Environment, HostedZoneName]
ValidationDomain: 'myblueprint.cloud'
CustomResourceLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: 'sts:AssumeRole'
Effect: Allow
Principal:
Service: 'lambda.amazonaws.com'
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
Policies:
- PolicyName: ApiGateway
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 'apigateway:*'
Effect: Allow
Resource: '*'
DomainNameInfoCustomResourceFunction:
Type: 'AWS::Lambda::Function'
Properties:
Handler: index.handler
Role: !GetAtt CustomResourceLambdaExecutionRole.Arn
Runtime: 'nodejs6.10'
Timeout: 300
Code:
ZipFile: |
const AWS = require('aws-sdk');
const response = require('cfn-response');
exports.handler = function(event, context) {
const ApiGateway = new AWS.APIGateway();
ApiGateway.getDomainName({
domainName: event.ResourceProperties.DomainName
}, (err, data) => {
if (err != null) {
response.send(event, context, response.FAILED, undefined);
} else {
response.send(event, context, response.SUCCESS, {
DomainName: data.domainName,
RegionalDomainName: data.regionalDomainName,
RegionalHostedZoneId: data.regionalHostedZoneId,
DistributionDomainName: data.distributionDomainName,
DistributionHostedZoneId: data.distributionHostedZoneId
});
}
});
}
ApiDomainName:
Type: 'AWS::ApiGateway::DomainName'
Properties:
DomainName:
'Fn::Sub':
- 'api.${HostedZoneName}'
- HostedZoneName: !FindInMap [Config, !Ref Environment, HostedZoneName]
EndpointConfiguration:
Types:
- REGIONAL
RegionalCertificateArn: !Ref ApiCertificate
ApiBasePathMapping:
Type: 'AWS::ApiGateway::BasePathMapping'
DependsOn: [MyBlueprintApi, ApiDomainName]
Properties:
DomainName:
'Fn::Sub':
- 'api.${HostedZoneName}'
- HostedZoneName: !FindInMap [Config, !Ref Environment, HostedZoneName]
RestApiId: !Ref MyBlueprintApi
Stage: 'prod'
ApiDomainNameInfo:
Type: 'Custom::DomainNameInfo'
DependsOn: [ApiDomainName, ApiBasePathMapping]
Properties:
ServiceToken: !GetAtt DomainNameInfoCustomResourceFunction.Arn
DomainName: !Ref ApiDomainName
ApiHealthCheck:
Type: 'AWS::Route53::HealthCheck'
Properties:
HealthCheckConfig:
Port: 443
Type: 'HTTPS_STR_MATCH'
SearchString: 'ok'
ResourcePath: '/prod/healthcheck'
FullyQualifiedDomainName: !Sub '${MyBlueprintApi}.execute-api.${AWS::Region}.amazonaws.com'
RequestInterval: 60
FailureThreshold: 2
HealthCheckTags:
- Key: Name
Value:
'Fn::Sub':
- 'api-regional-${EnvironmentLowerCase}-${Region}'
- EnvironmentLowerCase: !FindInMap [Config, !Ref Environment, EnvironmentLowerCase]
Region: !Ref 'AWS::Region'
ApiRecordSet:
Type: 'AWS::Route53::RecordSet'
DependsOn: [ApiDomainNameInfo]
Properties:
HostedZoneId: !FindInMap [Config, !Ref Environment, HostedZoneId]
Name:
'Fn::Sub':
- 'lbr-api.${HostedZoneName}'
- HostedZoneName: !FindInMap [Config, !Ref Environment, HostedZoneName]
ResourceRecords:
- !GetAtt ApiDomainNameInfo.RegionalDomainName
Region: !Ref 'AWS::Region'
SetIdentifier: !Sub 'api-${AWS::Region}'
HealthCheckId: !Ref ApiHealthCheck
Type: CNAME
TTL: 60
Outputs:
RestAPIID:
Description: Rest API ID
Value: !Ref MyBlueprintApi
ApiUrl:
Description: URL of your API endpoint
Value: !Ref ApiRecordSet
HealthcheckApiUrl:
Description: URL of your API health check endpoint
Value: !Sub 'https://${MyBlueprintApi}.execute-api.${AWS::Region}.amazonaws.com/prod/healthcheck'
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 3
- Comments: 33 (14 by maintainers)
@rayhaanq Give this a try in your cloudformation
yaml
Finally make it work after a long day suffer from this issue.
It’s a good idea to write another lambda for options method which response successful pre-light check.
I didn’t notice that AWS has mentioned it in their docs [https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html](how to cors)
I am using sam 0.5.0, and I am getting
403 Forbidden
forOPTIONS
callSame here, getting
403
forOPTIONS
callSAM CLI, version 0.6.0
Finally, got the root cause here. This is a bug on our end and was pretty difficult to find.
I pulled the head of master locally (equivalent to 0.3.0) and after some long digging found this while inspecting the Flask
self._app
inservice.py
.You can see OPTIONS is getting added to every request but there are many rules for each endpoint. The many rules is fine (just caused by the way we define a route). The interesting thing is, why does OPTIONS get appended to every route. Well in Flask 0.8, a
provide_automatic_options
functionality was added and is on by default. When this is enabled, Flask checks to see if the list of methods pass intoadd_url_rule
has an options method and adds it if it doesn’t. The reason/graphql
was only returning a 200 was because of this (Flask will not invoke the_request_handler
if this is enabled).There are a couple things, I think we need to do here:
add_url_rule
once per endpoint, since for the functions that would have added OPTIONS would have worked as expected.@charsleysa I apologize for not going deeper into this originally. The reason I didn’t catch this in the first place was due to not completely keeping the template the same (I scoped this down to make it easier for me to understand but didn’t keep the
AWS::Serverless::Function
’s identical. The problem is limited to endpoints defined with OPTIONS and other HTTP Verbs. You may not see the issue because it is dependent on the order weadd_url_rule
(why it worked for/healthcheck
but not/graphql
.Updating Labels to reflect current state.
Tracking issue for CORS is #323. See that issue for more details.
The original request here was Flask responding to OPTIONS requests which was solved and release: https://github.com/awslabs/aws-sam-cli/issues/400#issuecomment-398394463
There was addition reports of Flask still responding here: https://github.com/awslabs/aws-sam-cli/issues/400#issuecomment-433442194 but no response from those parties.
Closing at this looks to be solved and other comments relate to CORS support, which has it’s own issue
@cl0ckwork awesome, this worked. I just set up another function for options to return a success response. Thanks
Hi, I’ve got the same issue. Getting 403 for OPTIONS requests to my graphql endpoint for sam local. Is there any work around for this at the moment? CLI version 0.10.0 @jfuss
@andres-lowrie For whatever reason, the Flask version of 1.0.2 wasn’t getting picked up. This is something wrong with the installation on your system. If you are currently using pip to install, I would uninstall the CLI (
pip uninstall --user aws-sam-cli
andpip3 uninstall --user aws-sam-cli
if you have python3 as well) and move to our new installers. This will give you an isolated install of the CLI and ensure the correct versions of all dependencies are install correctly.@jfuss thanks for your work in finding the problem! That definitely was a hidden one.
@hobotroid thanks for your help as well!
@hobotroid Again there is no CORS support currently. OPTIONS behaves the same as GET or any other of the HTTP verbs. If this is not the case, please give us more details so we can reproduce. As it stands, I have not been able to observe any incorrect behavior, that is when I return a proxy response from a lambda through an OPTIONS verb, I get the correct response as the caller.
@charsleysa Make sure the debugger is setup correctly. If you are using VS code, you need to be setting the debugger to legacy, if I recall correctly. If you are seeing issues with the debugger, please file a separate issue.
As for the CloudFormation Parameter values, are you are needing/wanting to override values in the Env Vars? If so, you can still pass
--env-vars
to the command. If you are referring to something else, please cut an issue and explain the use-case@charsleysa Yes, I spent hours trying to get all my OPTIONS rules to work. But at best I could get 75%. It has something to do with the path parsing, but I still can’t figured out exactly what.