Skip to content

Commit 81a11ef

Browse files
Nnaga1jennyhliu
andauthored
CSD-100: Lambdas reporting InvalidSignatureException errors (#4375)
* CSD-100 off master * add lerna run prepublish to bootstrap --------- Co-authored-by: jennyhliu <jenny.h.liu@nasa.gov>
1 parent 5e832be commit 81a11ef

4 files changed

Lines changed: 102 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ Phase 2 — full apply to clean up old S3 objects and apply remaining changes:
4242
- **Security Vulnerabilities**
4343
- Upgraded package `lodash` to version 4.18.1.
4444
- Updated package overrides to address CVEs GHSA-43fc-jf86-j433 and GHSA-r5fr-rjxr-66jc.
45+
- **CSD-100**
46+
- made changes to the `PrivateApiLambda` and `ApiEndpoints` lambdas to ensure the environment variables
47+
are loaded after the handler invocation to circumvent `InvalidSignatureException` errors that were being reported
4548

4649
## [v21.3.2] 2026-03-20
4750

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"tsc": "lerna run tsc",
5050
"tsc:listEmittedFiles": "lerna run tsc:listEmittedFiles --stream --no-prefix",
5151
"package": "lerna run package",
52-
"bootstrap": "npm install && lerna run build && lerna run package && npm run bootstrap-python && git rev-parse --short HEAD > .bootstrapSha",
52+
"bootstrap": "npm install && lerna run build && lerna run package && npm run bootstrap-python && lerna run prepublish && git rev-parse --short HEAD > .bootstrapSha",
5353
"bootstrap-python": "uv sync --all-groups",
5454
"bootstrap-no-build": "npm install && npm run bootstrap-python",
5555
"bootstrap-no-build-no-scripts": "npm install --ignore-scripts && npm run bootstrap-python",

packages/api/app/index.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ const boom = require('../lib/expressBoom');
2121

2222
const log = new Logger({ sender: '@api/index' });
2323

24+
let initEnvVars;
25+
2426
// Load Environment Variables
25-
// This should be done outside of the handler to minimize Secrets Manager calls.
27+
// Called once per Lambda container to minimize Secrets Manager calls
2628
const initEnvVarsFunction = async () => {
2729
if (inTestMode() && process.env.INIT_ENV_VARS_FUNCTION_TEST !== 'true') {
2830
return undefined;
@@ -44,9 +46,19 @@ const initEnvVarsFunction = async () => {
4446
log.error(`Encountered error trying to set environment variables from secret ${apiConfigSecretId}`, error);
4547
throw error;
4648
}
49+
log.info('Environment variables successfully initialized');
4750
return undefined;
4851
};
49-
const initEnvVars = initEnvVarsFunction();
52+
53+
const ensureEnvVarsInitialized = () => {
54+
if (initEnvVars === undefined) {
55+
initEnvVars = initEnvVarsFunction().catch((error) => {
56+
initEnvVars = undefined; // allow retry
57+
throw error;
58+
});
59+
}
60+
return initEnvVars;
61+
};
5062

5163
// Setup express app
5264
const app = express();
@@ -95,7 +107,9 @@ app.use((err, _req, res, _next) => {
95107
const server = awsServerlessExpress.createServer(app);
96108

97109
const handler = async (event, context) => {
98-
await initEnvVars; // Wait for environment vars to resolve from initEnvVarsFunction
110+
// Ensures environment variables are initialized once per container;
111+
// subsequent invocations reuse the result or allow for re-initialization on failure
112+
await ensureEnvVarsInitialized();
99113
const dynamoTableNames = JSON.parse(getRequiredEnvVar('dynamoTableNameString'));
100114
// Set Dynamo table names as environment variables for Lambda
101115
Object.keys(dynamoTableNames).forEach((tableEnvVarName) => {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const test = require('ava');
2+
const sinon = require('sinon');
3+
4+
const awsServices = require('@cumulus/aws-client/services');
5+
const { randomString } = require('@cumulus/common/test-utils');
6+
7+
let secretId;
8+
const secretString = JSON.stringify({ testKey: 'testVal' });
9+
10+
test.before(() => {
11+
process.env.INIT_ENV_VARS_FUNCTION_TEST = 'true';
12+
secretId = randomString(10);
13+
process.env.api_config_secret_id = secretId;
14+
});
15+
16+
test.after.always(() => {
17+
delete process.env.INIT_ENV_VARS_FUNCTION_TEST;
18+
});
19+
20+
test.serial('secretsManager is not called at module load time and called once for multiple invocations', async (t) => {
21+
const stub = sinon.stub(awsServices, 'secretsManager');
22+
const fakeClient = {
23+
getSecretValue: sinon.stub().resolves({
24+
SecretString: secretString,
25+
}),
26+
};
27+
stub.returns(fakeClient);
28+
29+
// eslint-disable-next-line global-require
30+
const { handler } = require('../../app');
31+
process.env.dynamoTableNameString = '{}';
32+
33+
// since secretsManager is now called within the handler and not during module load
34+
// which is done on line 38 above, there should be no calls to secretsManager until
35+
// the handler itself is called
36+
t.is(fakeClient.getSecretValue.callCount, 0);
37+
await handler({});
38+
39+
// calling handler again to make sure the initPromise cache is working and
40+
// secretsManager itself is not called again
41+
await handler({});
42+
await handler({});
43+
44+
// since the handler ran, there should be one call to secretsManager, even if it
45+
// ran multiple times, the initPromise cache should prevent multiple calls to secretsManager
46+
t.is(fakeClient.getSecretValue.callCount, 1);
47+
t.teardown(() => stub.restore());
48+
});
49+
50+
test.serial('handler retries successfully after a secretsManager failure', async (t) => {
51+
const stub = sinon.stub(awsServices, 'secretsManager');
52+
53+
const fakeClient = {
54+
getSecretValue: sinon.stub().resolves({
55+
SecretString: secretString,
56+
}),
57+
};
58+
stub.returns(fakeClient);
59+
60+
fakeClient.getSecretValue.onFirstCall().rejects();
61+
fakeClient.getSecretValue.onSecondCall().resolves({
62+
SecretString: secretString,
63+
});
64+
65+
// refresh the module/app so the previous test doesn't affect this one
66+
// and then reimport the handler
67+
delete require.cache[require.resolve('../../app')];
68+
// eslint-disable-next-line global-require
69+
const { handler } = require('../../app');
70+
71+
t.is(fakeClient.getSecretValue.callCount, 0);
72+
await t.throwsAsync(handler({}));
73+
t.is(fakeClient.getSecretValue.callCount, 1);
74+
75+
await handler({});
76+
t.is(fakeClient.getSecretValue.callCount, 2);
77+
78+
await handler({});
79+
t.is(fakeClient.getSecretValue.callCount, 2);
80+
t.teardown(() => stub.restore());
81+
});

0 commit comments

Comments
 (0)