Repository: serverless/serverless (JavaScript)
Date: 2019-01-12 11:27:11+00:00
Task type: Bug Fix

Issue:
Issue with awsProvider.js : "Cannot use 'in' operator to search for '0'"
# This is a Bug Report

## Description

* What went wrong?

I'm deploying a package with the command "serverless deploy -v --stage cicd" with CodeBuild and I'm getting the error message below. I don't have much more information to give since I have no idea what's causing this issue.

The deployment is done by CodeBuild, Lerna is used to deploy several packages at once. Another package without any function is deployed properly before the failure for this one.

It is to be noted that the package was working fine yesterday. I reset the repository to a state CodeBuild already succeeded to build and delete the whole stack to be sure no change through the console has been done to the resources. The error is still appearing.

* What did you expect should have happened?
Proper deployment since it the yaml file has been proven to be able to be successfully built.

* What was the config you used?

serverless.yml:
 ```yaml
plugins:
  - serverless-webpack

# Enable auto-packing of external modules
custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: true
              
provider:
  name: aws
  runtime: nodejs8.10
  stage: dev
  region: eu-central-1
  environment:
    STAGE: ${opt:stage, self:provider.stage}
    BUCKET_NAME: <BucketName>
  iamRoleStatements:
    -  Effect: "Allow"
       Action:
         - "s3:ListBucket"
       Resource:
         Fn::Join:
           - ""
           - - "arn:aws:s3:::"
             - Ref: S3Bucket
    -  Effect: "Allow"
       Action:
         - "s3:GetObject"
       Resource:
         Fn::Join:
           - ""
           - - "arn:aws:s3:::"
             - Ref: S3Bucket
             - "/*"
  vpc:
    securityGroupIds:
      - ${cf:<otherPackageName>.SecurityGroup}
    subnetIds:
      - ${cf:<otherPackageName>.Subnet1}
      - ${cf:<otherPackageName>.Subnet2}
      - ${cf:<otherPackageName>.Subnet3}     
 
package:
  individually: true

functions:
    ${file(./serverless_functions.yml):functions}
  
resources:
  - ${file(./serverless_s3.yml)}
 ```
serverles_functions.yml:
 ```yaml
functions:
  <functionName>:
    handler: src/handler.main
    environment:
        DB_USER: ${file(./db_config.json):dbuser} 
        DB_PSWD: ${file(./db_config.json):dbpwd}
        DB_ENDPOINT: ${cf:<otherPackageName>.Endpoint}
    events:
      - S3:
            bucket: ${self:provider.environment.BUCKET_NAME}
            event: s3:ObjectCreated:*
            rules:
            - suffix: .csv
 ```
handler.js:

 ```javascript
import AWS from "aws-sdk";
export const main = (event, context, callback) => {
	console.log('start');
	var s3 = new AWS.S3();
	var params = {
		Bucket: process.env.BUCKET_NAME, 
		Key: decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '))
	};
	console.log('event Key:' + params.Key);
	console.log('retrieving object...');
	
	s3.getObject(params, function(err, data) {
		if (err) {
			console.log(err, err.stack) // an error occurred
		}else {
			console.log(data); 
		}
	});
}	
 ```
* What stacktrace or error message from your provider did you see?
  
  Serverless plugin "/codebuild/output/src417638977/src/packages/backend/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js" initialization errored: Cannot use 'in' operator to search for '0' in ${file(./serverless_functions.yml):functions} 


## Additional Data

* ***Serverless Framework Version you're using***: ^1.35.0
* ***Operating System***:  linux
* ***To Be Noted***:
The S3 event in serverless_functions.yml isn't working. While I will greatly appreciate if you find out why, it didn't prevent the deployment up until now so it shouldn't be related to the issue.


EDIT: the problem seems to come from V1.36, blocking version to V1.35 fixes the issue


Issue discussion:
same here, also occurs if the functions are in the same file but in a _different object_ like:

```
service: foo
...
functions: ${self:custom.lambdaFunctions}
custom:
  lambdaFunctions:
    bar:
      handler: ...
```

edit: only occurs in `v1.36.0` @dschep, downgrading to `v1.35.1` fixes this issue.
> edit: only occurs in `v1.36.0` @dschep, downgrading to `v1.35.1` fixes this issue.

I confirm, I locked the version to V1.35.1 and it's working fine now.

I confirmed this issue is due to changes in https://github.com/serverless/serverless/pull/5638 shipped in v1.36.0.
I will open a PR to fix it.

Same problem here.
Of course, I downgraded to `serverless@1.35.1`

# Anyway, Bug Report here

- serverless.yml
``` yml
service:
  name: react-client-ssr

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${env:NODE_ENV} # Set the default stage used. Default is dev
  region: ap-northeast-2 # Overwrite the default region used. Default is us-east-1
  memorySize: 512 # Overwrite the default memory size. Default is 1024
  environment:
    NODE_ENV: ${self:provider.stage}
  deploymentBucket:
    name: moducampus-client.sls.${self:provider.stage} # Overwrite the default deployment bucket
    serverSideEncryption: AES256 # when using server-side encryption

functions: ${file(./config/handlers.yml)}
```

- config/handlers.yml

``` yml
ssr:
  handler: src/handler.ssr
  events:
    - http: 
        path: /
        method: GET
    - http: 
        path: /{proxy+}
        method: ANY
    - http: 
        path: /{proxy+}
        method: OPTIONS

```

-------
output 

```
(...)
Serverless: Load command plugin:list
Serverless: Load command plugin
Serverless: Load command plugin:search
Serverless: Load command config
Serverless: Load command config:credentials
 
  Type Error ---------------------------------------------
 
  Cannot use 'in' operator to search for '0' in ${file(./config/handlers.yml)}
 
     For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable.
 
  Stack Trace --------------------------------------------
 
TypeError: Cannot use 'in' operator to search for '0' in ${file(./config/handlers.yml)}
    at Service.getFunction (/usr/local/lib/node_modules/serverless/lib/classes/Service.js:261:22)
    at Service.getAllEventsInFunction (/usr/local/lib/node_modules/serverless/lib/classes/Service.js:284:17)
    at AwsProvider.serverless.service.getAllFunctions.forEach.funcName (/usr/local/lib/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js:207:41)
    at Array.forEach (<anonymous>)
    at new AwsProvider (/usr/local/lib/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js:206:47)
    at PluginManager.addPlugin (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:52:28)
    at plugins.forEach (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:92:14)
    at Array.forEach (<anonymous>)
    at PluginManager.loadPlugins (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:88:13)
    at PluginManager.loadCorePlugins (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:129:10)
    at PluginManager.loadAllPlugins (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:83:10)
    at service.load.then (/usr/local/lib/node_modules/serverless/lib/Serverless.js:70:28)
    at <anonymous>
From previous event:
    at runCallback (timers.js:810:20)
    at tryOnImmediate (timers.js:768:5)
    at processImmediate [as _immediateCallback] (timers.js:745:5)
From previous event:
    at /usr/local/lib/node_modules/serverless/bin/serverless:29:46
    at Object.<anonymous> (/usr/local/lib/node_modules/serverless/bin/serverless:66:4)
    at Module._compile (module.js:653:30)
    at Object.Module._extensions..js (module.js:664:10)
    at Module.load (module.js:566:32)
    at tryModuleLoad (module.js:506:12)
    at Function.Module._load (module.js:498:3)
    at Function.Module.runMain (module.js:694:10)
    at startup (bootstrap_node.js:204:16)
    at bootstrap_node.js:625:3
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com
 
  Your Environment Information -----------------------------
     OS:                     linux
     Node Version:           8.12.0
     Serverless Version:     1.36.0
 
ERROR: Job failed: exit code 1
```
@gdugernier 

`functions:
   ${file(./serverless_functions.yml):functions}`

you can have a try as follow, i have done it .its ok now.  

functions:
   '- ${file(./serverless_functions.yml):functions}

Solution patch:
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 8e5d72d8a55..13a17d99880 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -6,6 +6,8 @@ const _ = require('lodash');
 const BbPromise = require('bluebird');
 const semver = require('semver');
 
+const validAPIGatewayStageNamePattern = /^[-_a-zA-Z0-9]+$/;
+
 class Service {
   constructor(serverless, data) {
     // #######################################################################
@@ -230,6 +232,22 @@ class Service {
     };
     warnOnDuplicateHandlers();
 
+    const provider = this.serverless.getProvider('aws');
+    if (provider) {
+      const stage = provider.getStage();
+      this.getAllFunctions().forEach(funcName => {
+        _.forEach(this.getAllEventsInFunction(funcName), event => {
+          if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
+            throw new this.serverless.classes.Error([
+              `Invalid stage name ${stage}:`,
+              'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present',
+              'according to API Gateway limitation.',
+            ].join(' '));
+          }
+        });
+      });
+    }
+
     return this;
   }
 
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 68cd4f4b4e1..940b44b4f1c 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -10,8 +10,6 @@ const fse = require('../utils/fs/fse');
 const logWarning = require('./Error').logWarning;
 const PromiseTracker = require('./PromiseTracker');
 
-const validAPIGatewayStageNamePattern = /^[-_a-zA-Z0-9]+$/;
-
 /**
  * Maintainer's notes:
  *
@@ -110,19 +108,6 @@ class Variables {
           this.populateValue(config.value, true) // populate
             .then(populated => _.assign(config, { populated })));
         return this.assignProperties(provider, prepopulations);
-      }).then(() => {
-        const stage = provider.getStage();
-        this.serverless.service.getAllFunctions().forEach(funcName => {
-          _.forEach(this.serverless.service.getAllEventsInFunction(funcName), event => {
-            if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
-              throw new this.serverless.classes.Error([
-                `Invalid stage name ${stage}:`,
-                'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present',
-                'according to API Gateway limitation.',
-              ].join(' '));
-            }
-          });
-        });
       });
     }
     return BbPromise.resolve();


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 22b9069201f..29b36566891 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -763,6 +763,141 @@ describe('Service', () => {
         expect(consoleLogStub.callCount).to.equal(2);
       });
     });
+
+    describe('stage name validation', () => {
+      function simulateRun(serverless) {
+        return serverless.init().then(() =>
+          serverless.variables.populateService(serverless.pluginManager.cliOptions)
+            .then(() => {
+              serverless.service.mergeArrays();
+              serverless.service.setFunctionNames(serverless.processedInput.options);
+            }));
+      }
+
+      it(`should not throw an error if http event is absent and 
+            stage contains only alphanumeric, underscore and hyphen`, () => {
+        const SUtils = new Utils();
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: 'xyz-101_abc-123',
+          },
+          functions: {
+            first: {
+              events: [],
+            },
+          },
+        };
+        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
+          YAML.dump(serverlessYml));
+
+        const serverless = new Serverless({ servicePath: tmpDirPath });
+        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
+          expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
+        });
+      });
+
+      it(`should not throw an error after variable population if http event is present and
+            the populated stage contains only alphanumeric, underscore and hyphen`, () => {
+        const SUtils = new Utils();
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: '${opt:stage, "default-stage"}',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
+          YAML.dump(serverlessYml));
+
+        const serverless = new Serverless({ servicePath: tmpDirPath });
+        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
+          expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
+        });
+      });
+
+      it('should throw an error if http event is present and stage contains invalid chars', () => {
+        const SUtils = new Utils();
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: 'my@stage',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
+          YAML.dump(serverlessYml));
+
+        const serverless = new Serverless({ servicePath: tmpDirPath });
+        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
+          expect(() => serverless.service.validate()).to.throw(serverless.classes.Error, [
+            'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
+            'for AWS provider if http event are present',
+            'according to API Gateway limitation.',
+          ].join(' '));
+        });
+      });
+
+      it(`should throw an error after variable population
+            if http event is present and stage contains hyphen`, () => {
+        const SUtils = new Utils();
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: '${opt:stage, "default:stage"}',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
+          YAML.dump(serverlessYml));
+
+        const serverless = new Serverless({ servicePath: tmpDirPath });
+        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
+          expect(() => serverless.service.validate()).to.throw(serverless.classes.Error, [
+            'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
+            'for AWS provider if http event are present',
+            'according to API Gateway limitation.',
+          ].join(' '));
+        });
+      });
+    });
   });
 
   describe('#mergeArrays', () => {
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index fdf91a57e5f..456d6187d48 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -207,113 +207,6 @@ describe('Variables', () => {
         });
       });
     });
-    describe('stage name validation', () => {
-      it(`should not throw an error if http event is absent and 
-            stage contains only alphanumeric, underscore and hyphen`, () => {
-        const serverlessYml = {
-          service: 'new-service',
-          provider: {
-            name: 'aws',
-            stage: 'xyz-101_abc-123',
-          },
-          functions: {
-            first: {
-              events: [],
-            },
-          },
-        };
-        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
-
-        return serverless.variables.populateService().should.be.fullfilled;
-      });
-
-      it(`should not throw an error after variable population if http event is present and
-            the populated stage contains only alphanumeric, underscore and hyphen`, () => {
-        const serverlessYml = {
-          service: 'new-service',
-          provider: {
-            name: 'aws',
-            stage: '${opt:stage, "default-stage"}',
-          },
-          functions: {
-            first: {
-              events: [
-                {
-                  http: {
-                    path: 'foo',
-                    method: 'GET',
-                  },
-                },
-              ],
-            },
-          },
-        };
-        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
-
-        return serverless.variables.populateService().should.be.fullfilled;
-      });
-
-      it('should throw an error if http event is present and stage contains invalid chars', () => {
-        const serverlessYml = {
-          service: 'new-service',
-          provider: {
-            name: 'aws',
-            stage: 'my@stage',
-          },
-          functions: {
-            first: {
-              events: [
-                {
-                  http: {
-                    path: 'foo',
-                    method: 'GET',
-                  },
-                },
-              ],
-            },
-          },
-        };
-        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
-
-        return serverless.variables.populateService().should.be
-          .rejectedWith(serverless.classes.Error, [
-            'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
-            'for AWS provider if http event are present',
-            'according to API Gateway limitation.',
-          ].join(' '));
-      });
-
-      it(`should throw an error after variable population
-            if http event is present and stage contains hyphen`, () => {
-        const serverlessYml = {
-          service: 'new-service',
-          provider: {
-            name: 'aws',
-            stage: '${opt:stage, "default:stage"}',
-          },
-          functions: {
-            first: {
-              events: [
-                {
-                  http: {
-                    path: 'foo',
-                    method: 'GET',
-                  },
-                },
-              ],
-            },
-          },
-        };
-        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
-
-        return serverless.variables.populateService().should.be
-          .rejectedWith(serverless.classes.Error, [
-            'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
-            'for AWS provider if http event are present',
-            'according to API Gateway limitation.',
-          ].join(' '));
-      });
-    });
   });
 
   describe('#getProperties', () => {