I have a CDK project to create a DynamoDB Table and an API Gateway Rest API that exposes the results of a query to that table. I need to make a GET request to that API from a different domain than the API's domain, so I have to add a CORS header. API Gateway provides an easy way to specify the CORS behaviour for the OPTIONS request, but from what I can tell it's my responsibility to add it for the other verbs. According to these docs:
When you enable CORS by using the AWS Management Console, API Gateway creates an OPTIONS method and attempts to add the Access-Control-Allow-Origin header to your existing method integration responses. This doesn’t always work, and sometimes you need to manually modify the integration response to properly enable CORS. Usually this just means manually modifying the integration response to return the Access-Control-Allow-Origin header.
Note that these docs describe what happens when you use the AWS Console, whereas I'm doing it with CDK. Although "API Gateway attempts to add the header" for console, I expect to have to do that myself. It seems that my task is to modify the integration response to return the Access-Control-Allow-Origin header (through CDK).
I have authored a CDK package that includes the CORS header for OPTIONS and no CORS header for the other verbs (see below). When I manually (through the web console) change the integration response to return the Access-Control-Allow-Origin header, I see the header in the response to the GET request as expected. However, when I try to reproduce that through CDK I cannot find a combination of integration response options that successfully builds and deploys. At the bottom of the question I include the CDK attempt I made, but it didn't deploy successfully (and neither did several other CDK options guesses that I made and discarded).
I'd like to know how I can replace my manual integration response changes with a CDK declaration that does the same thing.
CDK:
const table = new dynamodb.Table(this, 'NiftyTable', { partitionKey: ... });
const api = new apigateway.RestApi(this, 'NiftyApi', {
defaultCorsPreflightOptions: {
allowOrigins: [
'https://production.nifty.com',
'http://local.nifty.com:8080',
]
}
});
const specificResource = api.root.addResource('SomethingSpecific')
const dynamoQueryIntegration = new apigateway.AwsIntegration({ service: 'dynamodb', action: 'Query', options: {
passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_TEMPLATES,
credentialsRole: ...,
requestParameters: { ... },
requestTemplates: { 'application/json': JSON.stringify({ TableName: table.tableName, ... }) },
integrationResponses: [{
statusCode: '200',
// I've tried adding responseParameters and responseTemplates here without success
}],
}});
api.addMethod('GET', dynamoQueryIntegration, {
methodResponses: [{ statusCode: '200' }],
requestParameters: { ... },
});
When I deploy this stack and make a request to the API from Firefox, I see the following in the Network tab:
OPTIONS returns a valid CORS header ✅
OPTIONS https://apidomain.com/prod/SomethingSpecific
access-control-allow-origin
https://production.nifty.com
GET has no CORS header 👎🏻
GET https://apidomain.com/prod/SomethingSpecific
Response body is not available to scripts (Reason: CORS Missing Allow Origin)
Through the AWS Api Gateway console I add a CORS header to the resource:
Access-Control-Allow-Origin
Access-Control-Allow-Origin
Mapping value to '*'
Now, when I make the same request from the browser to the API, the OPTIONS
request still returns the expected CORS header, and now the GET
request includes the expected header access-control-allow-origin *
which I just added through Console 👍🏻. If I was comfortable with manual config steps after deploying, this would solve all my problems. However, I'd like this deployment to be fully handled by CDK with no manual steps, so I'm not satisfied having to manually make the change.
I read the docs for ApiIntegration
which show that it accepts ApiIntegrationProps
which contain a field options
of type IntegrationOptions
. The IntegrationOptions
object has integrationResponses
which is a list of IntegrationResponse
objects, each which contain fields responseParameters
and responseTemplates
. Since I'm trying to change the integrationResponse, I think this object is where I need to make a change.
I couldn't find any guidance in official docs or stack overflow for how to use responseParameters
and responseTemplates
. I'm looking for an example that I can copy, but haven't found anything.
I wanted to know what happens in the web console when I make the change, so I repeated the manual steps with the network tab opened and saw that when I changed the Method Response it called updateMethodResponse
with content string {"patchOperations":[{"op":"add","path":"/responseParameters/method.response.header.Access-Control-Allow-Origin"}]}
. Then when I changed the Integration Response it called updateIntegrationResponse
with content string {"patchOperations":[{"op":"add","path":"/responseParameters/method.response.header.Access-Control-Allow-Origin","value":"'*'"}]}
.
Console Step | What I entered | Corresponding CDK option (guessed, probably incorrect) |
---|---|---|
Method Response | Access-Control-Allow-Origin |
responseParameters key |
Integration Response | select Access-Control-Allow-Origin , enter '*' |
responseParameters value |
Based on that, I changed my CDK to the following:
const dynamoQueryIntegration = new apigateway.AwsIntegration({ service: 'dynamodb', action: 'Query', options: {
passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_TEMPLATES,
credentialsRole: apiGatewayIntegrationRole,
requestParameters: { ... },
requestTemplates: { 'application/json': JSON.stringify({ TableName: table.tableName, ... }) },
integrationResponses: [{
statusCode: '200',
responseParameters: {
'integration.response.header.Access-Control-Allow-Origin': "'*'"
},
}],
}});
when I attempt to deploy this, I get the error
Invalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression specified: integration.response.header.Access-Control-Allow-Origin] (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx; Proxy: null)
Is it possible to do what I'm trying to do? Maybe there is a hack using the SDK from a lambda-backed custom resource? I would accept any solution that runs as part of my CDK deploy, even if it's a bit of a hack.
Only AwsIntegration
's options.integrationResponses
response parameter is not enough for that.
You need allow header on addMethod
's methodResponses
as well
export class ApiDynamoStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// DynamoDB Table
const ddbTable = new Table(this, 'ApiDynamoTable', {
partitionKey: {name:'pk', type: AttributeType.STRING},
billingMode: BillingMode.PAY_PER_REQUEST,
removalPolicy: RemovalPolicy.DESTROY,
});
// RestApi
const restApi = new RestApi(this, 'ApiDynamoRestApi', {
defaultCorsPreflightOptions: {
allowOrigins: [
'https://production.nifty.com',
'http://local.nifty.com:8080',
]
}
});
const resource = restApi.root.addResource('{id}')
// Allow the RestApi to access DynamoDb by assigning this role to the integration
const integrationRole = new Role(this, 'IntegrationRole', {
assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
})
ddbTable.grantReadWriteData(integrationRole)
// GET Integration with DynamoDb
const dynamoQueryIntegration = new AwsIntegration({
service: 'dynamodb',
action: 'Query',
options: {
passthroughBehavior: PassthroughBehavior.WHEN_NO_TEMPLATES,
credentialsRole: integrationRole,
requestParameters: {
'integration.request.path.id': 'method.request.path.id'
},
requestTemplates: {
'application/json': JSON.stringify({
'TableName': ddbTable.tableName,
'KeyConditionExpression': 'pk = :v1',
'ExpressionAttributeValues': {
':v1': {'S': "$input.params('id')"}
}
}),
},
integrationResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Origin': "'*'",
},
}
],
}
})
resource.addMethod('GET', dynamoQueryIntegration, {
methodResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Origin': true
}
}
],
requestParameters: {
'method.request.path.id': true
}
})
}
}