Repository: serverless/serverless (JavaScript)
Date: 2021-04-08 08:14:21+00:00
Task type: Bug Fix

Issue:
sls invoke local passes null environment variables as "null" string to handlers
<!-- ⚠️⚠️ Acknowledge ALL below remarks -->
<!-- ⚠️⚠️ Request may not be processed if it doesn't meet outlined criteria -->

<!-- ⚠️⚠️ Ensure you're using *latest* version of a Framework -->
<!-- ⚠️⚠️ If you're uncertain you deal with a bug, ask first at https://forum.serverless.com -->
<!-- ⚠️⚠️ If your issue is influenced by a plugin, report at plugin repository, not here -->
<!-- ⚠️⚠️ Search existing issues to avoid creating duplicates  -->

<!-- ⚠️⚠️ Answer ALL the questions below -->

<!--
Q1: Describe the issue
-->

sls invoke local passes null environment variables as "null" string to handlers.

With the following definition:

```
  environment:
    FOO: ${env:FOO,null} 
```

I get the message as below, but I expected to get "Foo is null".

```
2021-04-08 14:51:51  INFO  Handler:23 - Foo is "null"
```

Here is a fragment of my handler code:

```
                String foo = System.getenv("FOO");
                if (foo == null) {
                        LOG.info("Foo is null");
                } else {
                        LOG.info("Foo is \"" + foo + "\"");
                }
```

The project is generated by "create -t aws-java-maven"  and applied the following change.

```
diff -u -r aws-java-maven.orig/serverless.yml aws-java-maven/serverless.yml
--- aws-java-maven.orig/serverless.yml	2021-04-08 14:32:02.000000000 +0900
+++ aws-java-maven/serverless.yml	2021-04-08 14:03:27.000000000 +0900
@@ -11,7 +11,7 @@
 #
 # Happy Coding!
 
-service: aws-java-maven.orig
+service: aws-java-maven
 # app and org for use with dashboard.serverless.com
 #app: your-app-name
 #org: your-org-name
@@ -46,8 +46,8 @@
 #            - "/*"
 
 # you can define service wide environment variables here
-#  environment:
-#    variable1: value1
+  environment:
+    FOO: ${env:FOO,null} 
 
 # you can add packaging information here
 package:
diff -u -r aws-java-maven.orig/src/main/java/com/serverless/Handler.java aws-java-maven/src/main/java/com/serverless/Handler.java
--- aws-java-maven.orig/src/main/java/com/serverless/Handler.java	2021-04-08 14:32:02.000000000 +0900
+++ aws-java-maven/src/main/java/com/serverless/Handler.java	2021-04-08 14:10:27.000000000 +0900
@@ -16,6 +16,12 @@
 	@Override
 	public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
 		LOG.info("received: {}", input);
+		String foo = System.getenv("FOO");
+		if (foo == null) {
+			LOG.info("Foo is null");
+		} else {
+			LOG.info("Foo is \"" + foo + "\"");
+		}
 		Response responseBody = new Response("Go Serverless v1.x! Your function executed successfully!", input);
 		return ApiGatewayResponse.builder()
 				.setStatusCode(200)
```

<!--
Q2: Provide (in below placeholder) FULL content of serverless.yml, ensuring that:
• It consistently reproduces described issue
• It's as minimal as possible
• There's no plugins involved (plugin related issues need to be reported at plugin repositories)
• Has sensitive parts masked out
-->

<details>
<summary><code>serverless.yml</code></summary>

```yaml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: aws-java-maven
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: '2'

provider:
  name: aws
  runtime: java8
  lambdaHashingVersion: 20201221

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
  environment:
    FOO: ${env:FOO,null} 

# you can add packaging information here
package:
  artifact: target/hello-dev.jar

functions:
  hello:
    handler: com.serverless.Handler
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - httpApi:
#          path: /users/create
#          method: get
#      - websocket: $connect
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp
#      - alb:
#          listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/
#          priority: 1
#          conditions:
#            host: example.com
#            path: /hello

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"
```

</details>

<!--
Q3: Provide (in below placeholder) FULL name and output of the command that exposes the problem.
       Note: Ensure SLS_DEBUG=* env var for verbose debug output
-->

<details>
<summary><b><code>sls  invoke local -f hello</code> output</b></summary>

```
% sls  invoke local -f hello
Serverless: In order to get human-readable output, please implement "toString()" method of your "ApiGatewayResponse" object.
2021-04-08 14:45:14  INFO  Handler:18 - received: {}

2021-04-08 14:45:14  INFO  Handler:23 - Foo is "null"

com.serverless.ApiGatewayResponse@4009e306
```

</details>

<!--
Q4: Provide (in below placeholder) output of serverless --version
-->

<b>Installed version</b>

```
Framework Core: 2.34.0
Plugin: 4.5.3
SDK: 4.2.2
Components: 3.8.2
```
The latest version https://github.com/serverless/serverless/commit/7072795a50ce87879089d41335641ac37d4abd09 from the repository.


Issue discussion:

Solution patch:
diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js
index 1a747343fda..486d62f428c 100644
--- a/lib/plugins/aws/invokeLocal/index.js
+++ b/lib/plugins/aws/invokeLocal/index.js
@@ -140,7 +140,13 @@ class AwsInvokeLocal {
   getConfiguredEnvVars() {
     const providerEnvVars = this.serverless.service.provider.environment || {};
     const functionEnvVars = this.options.functionObj.environment || {};
-    return _.merge(providerEnvVars, functionEnvVars);
+    const mergedVars = _.merge(providerEnvVars, functionEnvVars);
+    return Object.keys(mergedVars)
+      .filter((k) => mergedVars[k] != null)
+      .reduce((m, k) => {
+        m[k] = mergedVars[k];
+        return m;
+      }, {});
   }
 
   async loadEnvVars() {


Test patch:
diff --git a/test/unit/lib/plugins/aws/invokeLocal/index.test.js b/test/unit/lib/plugins/aws/invokeLocal/index.test.js
index 7569d5db070..795388f7f05 100644
--- a/test/unit/lib/plugins/aws/invokeLocal/index.test.js
+++ b/test/unit/lib/plugins/aws/invokeLocal/index.test.js
@@ -1565,6 +1565,31 @@ describe('test/unit/lib/plugins/aws/invokeLocal/index.test.js', () => {
         // Replaces
         // https://github.com/serverless/serverless/blob/95c0bc09421b869ae1d8fc5dea42a2fce1c2023e/test/unit/lib/plugins/aws/invokeLocal/index.test.js#L426-L441
       });
+
+      it('should not expose null environment variables', async () => {
+        const response = await runServerless({
+          fixture: 'invocation',
+          cliArgs: ['invoke', 'local', '--function', 'async'],
+          configExt: {
+            provider: {
+              environment: {
+                PROVIDER_LEVEL_VAR: null,
+              },
+            },
+            functions: {
+              fn: {
+                environment: {
+                  FUNCTION_LEVEL_VAR: null,
+                },
+              },
+            },
+          },
+        });
+        const stdoutAsJson = JSON.parse(response.stdoutData);
+        const stdoutBodyAsJson = JSON.parse(stdoutAsJson.body);
+        expect(stdoutBodyAsJson.env.PROVIDER_LEVEL_VAR).to.be.undefined;
+        expect(stdoutBodyAsJson.env.FUNCTION_LEVEL_VAR).to.be.undefined;
+      });
     });
   };