Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Access-Control-Allow-Methods being accumulated from previous definitions
# Bug Report

## Description

When deploying to AWS setting custom cors, the `Access-Control-Allow-Methods` header is not being set correctly on preflight request. When cors value is to `true` this doesn't happen, so it's an issue only when cors settings are manually set.

Let's say we have a cors.yml file:

```yaml
cors:
  origins: ${file(./config/${self:provider.stage}.yml):cors.origins}
  headers:
    - Content-Type
    - X-Amz-Date
    - Authorization
    - X-Api-Key
    - X-Amz-Security-Token
    - X-Amz-User-Agent
  allowCredentials: true
```

And then, three functions with three different HTTP methods, being declared in the follolwing sequence in the serverless.yml file:

```yaml
functions:
  firstFunction:
    handler: src/functions/first/first.handler
    events:
      - http:
          method: post
          path: v1/first
          cors: ${file(./resources/cors.yml)}
          authorizer: ${self:custom.authorizer}

  secondFunction:
    handler: src/functions/second/second.handler
    events:
      - http:
          method: put
          path: v1/second
          cors: ${file(./resources/cors.yml)}
          authorizer: ${self:custom.authorizer}

  thirdFunction:
    handler: src/functions/third/third.handler
    events:
      - http:
          method: delete
          path: v1/third
          cors: ${file(./resources/cors.yml)}
          authorizer: ${self:custom.authorizer}

```

This is how Serverless Framework is generating the header mappings, which I can confirm navigating to OPTIONS inside the resource on AWS console and checking Header Mappings values inside the Integration Response section:

* `/v1/first`: 'OPTIONS,POST'
* `/v1/second`: 'OPTIONS,POST,PUT'
* `/v1/third`: 'OPTIONS,POST,PUT,DELETE'

As it is easy to conclude, serverless.yml is actually accumulating HTTP methods from previous functions and including them when setting the header for the next functions, following the same order as it is declared in serverless.yml. So for an endpoint which method is DELETE we're actually getting on preflight request `Access-Control-Allow-Methods` allowing POST and PUT as well.


Discussion:

Resolving patch:
diff --git a/commitlint.config.js b/commitlint.config.js
index 07422529a3e..b062f6769d2 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -8,7 +8,7 @@ module.exports = {
     'footer-max-line-length': [2, 'always', 72],
     'header-max-length': [2, 'always', 72],
     'scope-case': [2, 'always', 'start-case'],
-    'scope-enum': [2, 'always', ['', 'Binary Installer']],
+    'scope-enum': [2, 'always', ['', 'Binary Installer', 'Variables']],
     'subject-case': [2, 'always', 'sentence-case'],
     'subject-empty': [2, 'never'],
     'subject-full-stop': [2, 'never', '.'],
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 67df20039d4..e007044e98f 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -10,6 +10,8 @@ const fse = require('../utils/fs/fse');
 const logWarning = require('./Error').logWarning;
 const PromiseTracker = require('./PromiseTracker');
 
+const resolvedValuesWeak = new WeakSet();
+
 /**
  * Maintainer's notes:
  *
@@ -576,7 +578,18 @@ class Variables {
       }
       ret = this.tracker.add(variableString, ret, propertyString);
     }
-    return ret;
+    return ret.then(resolvedValue => {
+      if (!Array.isArray(resolvedValue) && !_.isPlainObject(resolvedValue)) return resolvedValue;
+      if (resolvedValuesWeak.has(resolvedValue)) {
+        try {
+          return _.cloneDeep(resolvedValue);
+        } catch (error) {
+          return resolvedValue;
+        }
+      }
+      resolvedValuesWeak.add(resolvedValue);
+      return resolvedValue;
+    });
   }
 
   getValueFromSls(variableString) {


Test patch:
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index 8513177444a..7a9d4f54f56 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -226,6 +226,33 @@ describe('Variables', () => {
       });
     });
 
+    describe('ensure unique instances', () => {
+      it('should not produce same instances for same variable patters used more than once', () => {
+        serverless.variables.service.custom = {
+          settings1: '${file(~/config.yml)}',
+          settings2: '${file(~/config.yml)}',
+        };
+
+        const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true);
+        const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`);
+        const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns({
+          test: 1,
+          test2: 'test2',
+        });
+
+        return serverless.variables
+          .populateService()
+          .should.be.fulfilled.then(result => {
+            expect(result.custom.settings1).to.not.equal(result.custom.settings2);
+          })
+          .finally(() => {
+            fileExistsStub.restore();
+            realpathSync.restore();
+            readFileSyncStub.restore();
+          });
+      });
+    });
+
     describe('aws-specific syntax', () => {
       let awsProvider;
       let requestStub;