diff --git a/package-lock.json b/package-lock.json index d4bfbe5..7d3ccfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "eslint-config-prettier": "8.8.0", "eslint-plugin-prettier": "4.2.1", "husky": "^8.0.3", + "lambda-runtime": "file:./src/", "mocha": "10.2.0", "mock-http-server": "^1.4.5", "nyc": "^15.1.0", @@ -3222,6 +3223,10 @@ "node": ">=6" } }, + "node_modules/lambda-runtime": { + "resolved": "src", + "link": true + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5275,6 +5280,9 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "src": { + "dev": true } } } diff --git a/package.json b/package.json index 8a7374b..cfb334d 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "node-gyp": "9.4.0" }, "devDependencies": { + "lambda-runtime": "file:./src/", "esbuild": "^0.18.3", "eslint": "8.42.0", "eslint-config-prettier": "8.8.0", diff --git a/src/RAPIDClient.js b/src/RAPIDClient.js index 84ce5f3..cf02787 100644 --- a/src/RAPIDClient.js +++ b/src/RAPIDClient.js @@ -44,7 +44,7 @@ module.exports = class RAPIDClient { */ postInvocationResponse(response, id, callback) { let bodyString = _trySerializeResponse(response); - this.nativeClient.done(id, bodyString); + this.nativeClient.done(encodeURIComponent(id), bodyString); callback(); } @@ -65,7 +65,10 @@ module.exports = class RAPIDClient { hostname: this.hostname, method: 'POST', port: this.port, - path: '/2018-06-01/runtime/invocation/' + id + '/response', + path: + '/2018-06-01/runtime/invocation/' + + encodeURIComponent(id) + + '/response', highWaterMark: options?.highWaterMark, }, }); @@ -108,7 +111,7 @@ module.exports = class RAPIDClient { let response = Errors.toRapidResponse(error); let bodyString = _trySerializeResponse(response); let xrayString = XRayError.formatted(error); - this.nativeClient.error(id, bodyString, xrayString); + this.nativeClient.error(encodeURIComponent(id), bodyString, xrayString); callback(); } diff --git a/test/unit/ErrorsTest.js b/test/unit/ErrorsTest.js index 9c0f9a5..8f88ae6 100644 --- a/test/unit/ErrorsTest.js +++ b/test/unit/ErrorsTest.js @@ -5,7 +5,7 @@ 'use strict'; require('should'); -let Errors = require('../../src/Errors'); +let Errors = require('lambda-runtime/Errors'); describe('Formatted Error Logging', () => { it('should fall back to a minimal error format when an exception occurs', () => { diff --git a/test/unit/FakeTelemetryTarget.js b/test/unit/FakeTelemetryTarget.js index e158dbe..21b38fc 100644 --- a/test/unit/FakeTelemetryTarget.js +++ b/test/unit/FakeTelemetryTarget.js @@ -112,6 +112,7 @@ module.exports = class FakeTelemetryTarget { if (lineLength === 0) { return ''; } + let lineBytes = Buffer.alloc(lineLength); let actualLineSize = fs.readSync( this.readTarget, diff --git a/test/unit/InvokeContextTest.js b/test/unit/InvokeContextTest.js index 58b35a2..c351b5d 100644 --- a/test/unit/InvokeContextTest.js +++ b/test/unit/InvokeContextTest.js @@ -7,7 +7,7 @@ require('should'); const sleep = require('util').promisify(setTimeout); -let InvokeContext = require('../../src/InvokeContext'); +let InvokeContext = require('lambda-runtime/InvokeContext'); describe('Getting remaining invoke time', () => { it('should reduce by at least elapsed time', async () => { diff --git a/test/unit/LogPatchTest.js b/test/unit/LogPatchTest.js index 08c8eca..7a8d2f6 100644 --- a/test/unit/LogPatchTest.js +++ b/test/unit/LogPatchTest.js @@ -7,8 +7,8 @@ const util = require('util'); let should = require('should'); -let LogPatch = require('../../src/LogPatch'); -let Errors = require('../../src/Errors'); +let LogPatch = require('lambda-runtime/LogPatch'); +let Errors = require('lambda-runtime/Errors'); let assert = require('assert'); let { diff --git a/test/unit/RAPIDClientTest.js b/test/unit/RAPIDClientTest.js new file mode 100644 index 0000000..828bd6d --- /dev/null +++ b/test/unit/RAPIDClientTest.js @@ -0,0 +1,117 @@ +/** + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +'use strict'; + +require('should'); + +let RAPIDClient = require('lambda-runtime/RAPIDClient.js'); +let runtimeErrors = require('lambda-runtime/Errors.js'); + +/** + * Stub request object. + * Provides no-op definitions of the request functions used by the rapid client. + */ +const noOpRequest = Object.freeze({ + /* no op, return itself to allow continuations/chaining */ + on: () => noOpRequest, + /* no op, return itself to allow continuations/chaining */ + end: () => noOpRequest, +}); + +class StubHttp { + constructor() { + this.lastUsedOptions = {}; + this.Agent = class FakeAgent {}; + } + + request(options, _callback) { + this.lastUsedOptions = options; + return noOpRequest; + } +} + +class NoOpNativeHttp { + constructor() { + this.lastRequestId = ''; + this.lastErrorRequestId = ''; + } + + done(requestId) { + this.lastRequestId = requestId; + } + + error(requestId) { + this.lastErrorRequestId = requestId; + } +} + +class EvilError extends Error { + get name() { + throw 'gotcha'; + } +} + +const EXPECTED_ERROR_HEADER = 'Lambda-Runtime-Function-Error-Type'; + +describe('building error requests with the RAPIDClient', () => { + let stubHttp = new StubHttp(); + let client = new RAPIDClient('notUsed:1337', stubHttp, new NoOpNativeHttp()); + + let errors = [ + [new Error('generic failure'), 'Error'], + [new runtimeErrors.ImportModuleError(), 'Runtime.ImportModuleError'], + [new runtimeErrors.HandlerNotFound(), 'Runtime.HandlerNotFound'], + [new runtimeErrors.MalformedHandlerName(), 'Runtime.MalformedHandlerName'], + [new runtimeErrors.UserCodeSyntaxError(), 'Runtime.UserCodeSyntaxError'], + [{ data: 'some random object' }, 'object'], + [new EvilError(), 'handled'], + ]; + + describe('the error header in postInitError', () => { + errors.forEach(([error, name]) => { + it(`should be ${name} for ${error.constructor.name}`, () => { + client.postInitError(error); + stubHttp.lastUsedOptions.should.have + .property('headers') + .have.property(EXPECTED_ERROR_HEADER, name); + }); + }); + }); +}); + +describe('invalid request id works', () => { + const nativeClient = new NoOpNativeHttp(); + const client = new RAPIDClient('notUsed:1337', undefined, nativeClient); + + [ + // Encoding expected: + ['#', '%23'], + ['%', '%25'], + ['/', '%2F'], + ['?', '%3F'], + ['\x7F', '%7F'], + ["", "%3Cscript%3Ealert('1')%3C%2Fscript%3E"], + ['⚡', '%E2%9A%A1'], + + // No encoding: + ['.', '.'], + ['..', '..'], + ['a', 'a'], + [ + '59b22c65-fa81-47fb-a6dc-23028a63566f', + '59b22c65-fa81-47fb-a6dc-23028a63566f', + ], + ].forEach(([requestId, expected]) => { + it(`postInvocationResponse should encode requestId: '${requestId}'`, () => { + client.postInvocationResponse({}, requestId, () => {}); + nativeClient.lastRequestId.should.be.equal(expected); + }); + + it(`postInvocationError should encode requestId: '${requestId}'`, () => { + client.postInvocationError(new Error(), requestId, () => {}); + nativeClient.lastErrorRequestId.should.be.equal(expected); + }); + }); +}); diff --git a/test/unit/ResponseStreamTest.js b/test/unit/ResponseStreamTest.js index dbfe940..b5d46c2 100644 --- a/test/unit/ResponseStreamTest.js +++ b/test/unit/ResponseStreamTest.js @@ -9,10 +9,10 @@ const ServerMock = require('mock-http-server'); const { createResponseStream, tryCallFail, -} = require('../../src/ResponseStream.js'); -const { HttpResponseStream } = require('../../src/HttpResponseStream.js'); -const { InvalidStreamingOperation } = require('../../src/Errors.js'); -const { verbose, vverbose, vvverbose } = require('../../src/VerboseLog').logger( +} = require('lambda-runtime/ResponseStream.js'); +const { HttpResponseStream } = require('lambda-runtime/HttpResponseStream.js'); +const { InvalidStreamingOperation } = require('lambda-runtime/Errors.js'); +const { verbose, vverbose, vvverbose } = require('lambda-runtime/VerboseLog').logger( 'TEST', ); const Throttle = require('throttle'); diff --git a/test/unit/StreamingContextTest.js b/test/unit/StreamingContextTest.js index b9e67a0..833ac8f 100644 --- a/test/unit/StreamingContextTest.js +++ b/test/unit/StreamingContextTest.js @@ -5,9 +5,9 @@ 'use strict'; require('should'); -const StreamingContext = require('../../src/StreamingContext.js'); +const StreamingContext = require('lambda-runtime/StreamingContext.js'); const { PassThrough } = require('stream'); -const BeforeExitListener = require('../../src/BeforeExitListener.js'); +const BeforeExitListener = require('lambda-runtime/BeforeExitListener.js'); class MockRapidClient { constructor() { diff --git a/test/unit/UserFunctionTest.js b/test/unit/UserFunctionTest.js index cf53b09..6ffe4c0 100644 --- a/test/unit/UserFunctionTest.js +++ b/test/unit/UserFunctionTest.js @@ -10,8 +10,8 @@ const { HandlerNotFound, ImportModuleError, MalformedHandlerName, -} = require('../../src/Errors.js'); -const UserFunction = require('../../src/UserFunction.js'); +} = require('lambda-runtime/Errors.js'); +const UserFunction = require('lambda-runtime/UserFunction.js'); const TEST_ROOT = path.join(__dirname, '../'); const HANDLERS_ROOT = path.join(TEST_ROOT, 'handlers');