How do you use Serverless to create an AWS API Gateway that can be accessed via a browser?
I tried following these serverless docs that describe how to allow CORS on an API Gateway but they don’t seem to work at all.
I wrote a serverless.yml to create a small search service with a /query
endpoint like:
org: myorg
service: myservice
variablesResolutionMode: 20210219
# NOTE: Remember to define your environment variables for the custom fields below.
custom:
name: ${sls:stage}-${self:service}
region: ${opt:region, "us-east-1"}
vpcId: ${env:VPC_ID}
subnetId1: ${env:SUBNET_ID1}
subnetId2: ${env:SUBNET_ID2}
javaVersion: provided.al2
provider:
name: aws
profile: ${env:PROFILE}
region: ${self:custom.region}
versionFunctions: false
apiGateway:
shouldStartNameWithService: true
tracing:
lambda: false
timeout: 15
environment:
stage: prod
DISABLE_SIGNAL_HANDLERS: true
iam:
role:
statements: ${file(roleStatements.yml)}
vpc:
securityGroupIds:
- Ref: EfsSecurityGroup
subnetIds:
- ${self:custom.subnetId1}
- ${self:custom.subnetId2}
package:
individually: true
functions:
query:
name: ${self:custom.name}-query
runtime: ${self:custom.javaVersion}
handler: native.handler
provisionedConcurrency: 1
memorySize: 256
events:
- http:
path: /query
method: post
cors: true
- http:
path: /query
method: options
cors: true
dependsOn:
- EfsMountTarget1
- EfsMountTarget2
- EfsAccessPoint
fileSystemConfig:
localMountPath: /mnt/data
arn:
Fn::GetAtt: [EfsAccessPoint, Arn]
package:
artifact: target/function.zip
environment:
DOMAIN_ORIGIN: "${env:LUCENE_SERVERLESS_QUERY_DOMAIN_ORIGIN}"
QUARKUS_LAMBDA_HANDLER: query
QUARKUS_PROFILE: prod
index:
name: ${self:custom.name}-index
runtime: ${self:custom.javaVersion}
handler: native.handler
reservedConcurrency: 1
memorySize: 256
timeout: 180
dependsOn:
- EfsMountTarget1
- EfsMountTarget2
- EfsAccessPoint
fileSystemConfig:
localMountPath: /mnt/data
arn:
Fn::GetAtt: [EfsAccessPoint, Arn]
package:
artifact: target/function.zip
environment:
QUARKUS_LAMBDA_HANDLER: index
QUARKUS_PROFILE: prod
events:
- sqs:
arn:
Fn::GetAtt: [WriteQueue, Arn]
batchSize: 5000
maximumBatchingWindow: 5
enqueue-index:
name: ${self:custom.name}-enqueue-index
runtime: ${self:custom.javaVersion}
handler: native.handler
memorySize: 256
package:
artifact: target/function.zip
vpc:
securityGroupIds: []
subnetIds: []
events:
- http: POST /index
environment:
QUARKUS_LAMBDA_HANDLER: enqueue-index
QUARKUS_PROFILE: prod
QUEUE_URL:
Ref: WriteQueue
delete-index:
name: ${self:custom.name}-delete-index
runtime: ${self:custom.javaVersion}
handler: native.handler
memorySize: 256
dependsOn:
- EfsMountTarget1
- EfsMountTarget2
- EfsAccessPoint
fileSystemConfig:
localMountPath: /mnt/data
arn:
Fn::GetAtt: [EfsAccessPoint, Arn]
package:
artifact: target/function.zip
environment:
QUARKUS_LAMBDA_HANDLER: deleteIndex
QUARKUS_PROFILE: prod
resources:
Resources:
WriteQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:custom.name}-write-queue
VisibilityTimeout: 900
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt: [WriteDLQ, Arn]
maxReceiveCount: 5
WriteDLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:custom.name}-write-dlq
MessageRetentionPeriod: 1209600 # 14 days in seconds
FileSystem:
Type: AWS::EFS::FileSystem
Properties:
BackupPolicy:
Status: DISABLED
FileSystemTags:
- Key: Name
Value: ${self:custom.name}-fs
PerformanceMode: generalPurpose
ThroughputMode: elastic # faster scale up/down
Encrypted: true
FileSystemPolicy:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "elasticfilesystem:ClientMount"
Principal:
AWS: "*"
EfsSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: ${self:custom.vpcId}
GroupDescription: "mnt target sg"
SecurityGroupIngress:
- IpProtocol: -1
CidrIp: "0.0.0.0/0"
- IpProtocol: -1
CidrIpv6: "::/0"
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: "0.0.0.0/0"
- IpProtocol: -1
CidrIpv6: "::/0"
EfsMountTarget1:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: ${self:custom.subnetId1}
SecurityGroups:
- Ref: EfsSecurityGroup
EfsMountTarget2:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: ${self:custom.subnetId2}
SecurityGroups:
- Ref: EfsSecurityGroup
EfsAccessPoint:
Type: "AWS::EFS::AccessPoint"
Properties:
FileSystemId: !Ref FileSystem
PosixUser:
Uid: "1000"
Gid: "1000"
RootDirectory:
CreationInfo:
OwnerGid: "1000"
OwnerUid: "1000"
Permissions: "0777"
Path: "/mnt/data"
DependsOn:
- EfsMountTarget1
- EfsMountTarget2
Running sls deploy
runs without error and I can access /query
via curl just fine with a call like:
curl -H "Content-Type: application/json" -X POST -d '{"indexName":"myindex","query": "some query term"}' https://myappid.execute-api.us-east-1.amazonaws.com/dev/query
However, if I make the equivalent call from Javascript in the browser like:
function search(text) {
$.ajax({
url: 'https://myappid.execute-api.us-east-1.amazonaws.com/dev/query?'+new Date().getTime(),
type: 'POST',
dataType: 'json',
data: {
'indexName': 'myindex',
'query': text
},
success: function(data) {
console.log('success');
console.log(data);
},
error: function(xhr, status, error) {
console.log('error');
}
});
}
search('some search term');
I get the infamous CORS error:
Access to XMLHttpRequest at 'https://myappid.execute-api.us-east-1.amazonaws.com/dev/query?1724248946289' from origin 'http://www.example.com:8111' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Why am I getting this error? I have cors: true
for both the post and options methods for /query
.
If I manually check the setup in AWS Console, it configures it as a proxy integration, so I don’t see any CORS header mappings under the Integration Response tabs.
Attempted Solution 1: Switch from the default proxy-handler to non-proxy.
I tried switch from a proxy to a direct lambda handler, but then the API Gateway is unable to pass the request to the lambda, and my lambda complains of either no input or malformed input.
I’ve tried adding these request template mappings for the application/json
content type:
{"body": $input.json('$')}
$input.body
{"body": "$util.escapeJavaScript($input.body)"}
{"body": "$input.body"}
{
"indexName": "$input.json('$.indexName')",
"query": "$input.json('$.query')"
}
{
"indexName": $input.json('$.indexName'),
"query": $input.json('$.query')
}
$input.json('$')
{"content": $input.json('$')}
but they either send over a corrupt request or no request.
How do I get the API Gateway to work with CORS?
Edit: I tweaked the way my Java lambda response handler added headers, and generalized them a little to:
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
headers.put("Access-Control-Allow-Credentials", "true");
headers.put("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
headers.put("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
try {
return response.withStatusCode(200).withHeaders(headers).withBody(queryResponseWriter.writeValueAsString(queryResponse));
} catch (JsonProcessingException e) {
LOG.error(e);
return response.withStatusCode(500).withHeaders(headers).withBody("Internal error");
}
Now if I make the ajax request, I can confirm it makes it to the lambda, becaues a CloudWatch log entry is generated, but I think it’s first querying OPTIONS, not POST, because my handler is logging blank content. Plus, the browser error has now changed to:
www.example.com/:1 Access to XMLHttpRequest at 'https://myappid.execute-api.us-east-1.amazonaws.com/dev/query?1724255655036' from origin 'http://www.example.com:8111' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
The “Response to preflight request doesn’t pass access control check” is new. I’m guessing my lambda isn’t handling the OPTIONS request, which I believe also needs to respond with the appropriate CORS headers, and that’s also failing.
Can the OPTIONS handler return the exact same CORS headers as GET and POST?