Repository: serverless/serverless (JavaScript) — Feature

Issue:
Feature: Add CacheControl to cors configuration to allow for OPTIONS caching in CloudFront
# This is a Feature Proposal

## Description

There is support for the maxAge header which is used by the browser to cache OPTIONS request.  In addition to this, I would like to cache my OPTIONS response in CloudFront to remove the extra retrieval from API Gateway.

By default, when `cors: true` is set the cache-control header would not be present.  I think it would be an advanced field that the user would have to specifically override like

```
cors:
  cacheControl: 'max-age=600'
```

I could get this change in pretty quickly as a PR if the structure is okay with everyone.

## Additional Data

There is currently a workaround where I could set the DefaultTTL on CloudFront, but there are other GETs that don't have a Cache-Control header passed back and I don't want them to get that DefaultTTL.



Discussion:

Resolving patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index a85408fe84e..5ab14febf6a 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -246,6 +246,31 @@ functions:
             maxAge: 86400
 ```
 
+If you are using CloudFront or another CDN for your API Gateway, you may want to setup a `Cache-Control` header to allow for OPTIONS request to be cached to avoid the additional hop.  
+
+To enable the `Cache-Control` header on preflight response, set the `cacheControl` property in the `cors` object:
+
+```yml
+functions:
+  hello:
+    handler: handler.hello
+    events:
+      - http:
+          path: hello
+          method: get
+          cors:
+            origin: '*'
+            headers:
+              - Content-Type
+              - X-Amz-Date
+              - Authorization
+              - X-Api-Key
+              - X-Amz-Security-Token
+              - X-Amz-User-Agent
+            allowCredentials: false
+            cacheControl: 'max-age=600, s-maxage=600, proxy-revalidate' # Caches on browser and proxy for 10 minutes and doesnt allow proxy to serve out of date content
+```
+
 If you want to use CORS with the lambda-proxy integration, remember to include the `Access-Control-Allow-*` headers in your headers object, like this:
 
 ```javascript
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js
index 4fe96e7f484..1f467b64eff 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js
@@ -34,6 +34,11 @@ module.exports = {
         }
       }
 
+      // Allow Cache-Control header if set
+      if (_.has(config, 'cacheControl')) {
+        preflightHeaders['Cache-Control'] = `'${config.cacheControl}'`;
+      }
+
       if (_.includes(config.methods, 'ANY')) {
         preflightHeaders['Access-Control-Allow-Methods'] =
           preflightHeaders['Access-Control-Allow-Methods']


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js
index 128b0375eea..6b45f83ffe5 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js
@@ -68,6 +68,7 @@ describe('#compileCors()', () => {
         methods: ['OPTIONS', 'PUT'],
         allowCredentials: false,
         maxAge: 86400,
+        cacheControl: 'max-age=600, s-maxage=600',
       },
       'users/create': {
         origins: ['*', 'http://example.com'],
@@ -75,6 +76,7 @@ describe('#compileCors()', () => {
         methods: ['OPTIONS', 'POST'],
         allowCredentials: true,
         maxAge: 86400,
+        cacheControl: 'max-age=600, s-maxage=600',
       },
       'users/delete': {
         origins: ['*'],
@@ -82,6 +84,7 @@ describe('#compileCors()', () => {
         methods: ['OPTIONS', 'DELETE'],
         allowCredentials: false,
         maxAge: 86400,
+        cacheControl: 'max-age=600, s-maxage=600',
       },
       'users/any': {
         origins: ['http://example.com'],
@@ -127,6 +130,13 @@ describe('#compileCors()', () => {
           .ResponseParameters['method.response.header.Access-Control-Max-Age']
       ).to.equal('\'86400\'');
 
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreateOptions
+          .Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Cache-Control']
+      ).to.equal('\'max-age=600, s-maxage=600\'');
+
       // users/update
       expect(
         awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
@@ -156,6 +166,13 @@ describe('#compileCors()', () => {
           .ResponseParameters['method.response.header.Access-Control-Max-Age']
       ).to.equal('\'86400\'');
 
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersUpdateOptions
+          .Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Cache-Control']
+      ).to.equal('\'max-age=600, s-maxage=600\'');
+
       // users/delete
       expect(
         awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
@@ -192,6 +209,13 @@ describe('#compileCors()', () => {
           .ResponseParameters['method.response.header.Access-Control-Max-Age']
       ).to.equal('\'86400\'');
 
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersDeleteOptions
+          .Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Cache-Control']
+      ).to.equal('\'max-age=600, s-maxage=600\'');
+
       // users/any
       expect(
         awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate