Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ concurrency:

jobs:
verify:
name: Node ${{ matrix.node }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
name: Node ${{ matrix.node }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['22', '24']
steps:
- uses: actions/checkout@v4
Expand All @@ -31,3 +30,66 @@ jobs:

- name: Test
run: npm test

runtime-init:
name: Lambda Node 24 runtime init smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm

- run: npm ci --omit=dev

# Initialize each handler inside the real Lambda Node.js 24 runtime image
# (Runtime Interface Emulator bundled). This exercises the same init code
# path (errorOnDeprecatedCallback, ImportModuleError, etc.) that runs in
# production — catches callback-shape regressions and other runtime-init
# incompatibilities that plain `node` cannot surface.
- name: Probe handlers in Lambda Node 24 runtime
run: |
set -euo pipefail

probe() {
local handler=$1
echo "::group::Probing $handler"

local container
container=$(docker run -d --rm -p 9000:8080 \
-v "$PWD":/var/task:ro \
public.ecr.aws/lambda/nodejs:24 \
"$handler")

local response=""
for _ in $(seq 1 60); do
if response=$(curl -s -m 1 -X POST \
http://localhost:9000/2015-03-31/functions/function/invocations \
-d '{}' 2>/dev/null) && [ -n "$response" ]; then
break
fi
sleep 0.5
done

echo "response: $response"

local exit_code=0
# Runtime.* errorTypes are init-level errors (CallbackHandlerDeprecated,
# ImportModuleError, MalformedHandlerName, ...). Handler-side errors
# from invoking with an empty event use different errorTypes and are
# expected here — we only care that init succeeded.
if echo "$response" | grep -Eq '"errorType":[[:space:]]*"Runtime\.'; then
echo "::error::$handler failed Lambda Node 24 runtime init"
docker logs "$container" 2>&1 || true
exit_code=1
fi

docker stop "$container" >/dev/null 2>&1 || true
echo "::endgroup::"
return $exit_code
}

probe lib/s3-handler.handler
probe lib/ag-handler.handler
16 changes: 7 additions & 9 deletions lib/ag-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ const pkg = require('../package.json');
* Handles HTTP result callback
* @param {Object} event the API gateway event to be processed
* @param context
* @param callback
*/
exports.handler = async (event, context, callback) => {
exports.handler = async (event, context) => {
console.log(`handling callback event using ${pkg.name}/v${pkg.version}`);

try {
Expand Down Expand Up @@ -40,26 +39,25 @@ exports.handler = async (event, context, callback) => {
}
}

// returning callback
callback(null, {
console.log("handling completed");

return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({status: "OK"}, null, 2)
});

console.log("handling completed");
};

} catch (error) {
console.log("something went wrong...");
console.error(error); // an error occurred
callback(null, {
return {
"statusCode": 500,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({status: error.message}, null, 2)
});
};
}
};
13 changes: 5 additions & 8 deletions lib/s3-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ const s3Client = new S3Client({});
* Handles events from S3 and submits object for processing
* @param event {Object} S3 event to be processed
* @param context the AWS lambda context
* @param callback AWS lambda callback
*/
exports.handler = async (event, context, callback) => {
exports.handler = async (event, context) => {

try {
console.log(`handling s3 event using ${pkg.name}/v${pkg.version}`);
Expand Down Expand Up @@ -65,24 +64,22 @@ exports.handler = async (event, context, callback) => {
assert.ok(submitResult.resourceLocation !== undefined, "invalid response from server, no response received");
console.log(`contents submitted for processing with id: ${submitResult.id} and location: ${submitResult.resourceLocation}`);


// returning back to
callback(null, {
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({status: "OK"}, null, 2)
});
};
} catch (error) {
console.error(error, error.stack); // an error occurred
callback(null, {
return {
"statusCode": 500,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({status: error.message}, null, 2)
});
};
}
};

Expand Down
110 changes: 42 additions & 68 deletions tests/ag-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ const { S3Client, DeleteObjectCommand, PutObjectTaggingCommand, GetObjectTagging
const s3Mock = mockClient(S3Client);

describe('Api Gateway handler tests', () => {
// AWS Lambda Node.js 24+ rejects async handlers that declare a `callback` param
// (Runtime.CallbackHandlerDeprecated). handler.length encodes that arity — keep
// it at ≤ 2 so init never regresses.
it('handler signature is compatible with Lambda Node.js 24+', () => {
assert.ok(handler.length <= 2,
`handler accepts ${handler.length} params; Lambda Node.js 24+ rejects callback-based handlers`);
});

beforeEach(() => {
s3Mock.reset();

Expand All @@ -31,7 +39,7 @@ describe('Api Gateway handler tests', () => {
});

it('should handle a callback without findings', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -43,25 +51,18 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 200);
});
}), {});
assert(result.statusCode === 200);
});

it('should handle a bogus callback', async () => {
await handler(hydrateEvent({"hello": "world"}),
{}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 500, "should return the file id");
assert(result.body.includes("no id provided"));
});
const result = await handler(hydrateEvent({"hello": "world"}), {});
assert(result.statusCode === 500, "should return the file id");
assert(result.body.includes("no id provided"));
});

it('should require bucket/key in callback metadata', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -71,18 +72,13 @@ describe('Api Gateway handler tests', () => {
"metadata": {
"signature": utils.generateSignature("test-bucket", "test-key"),
}
}),
{}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 500, "should return the file id");
assert(result.body.includes("no bucket supplied in metadata"));
});
}), {});
assert(result.statusCode === 500, "should return the file id");
assert(result.body.includes("no bucket supplied in metadata"));
});

it('should handle callbacks with findings', async () => {

await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -94,15 +90,12 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 200);
});
}), {});
assert(result.statusCode === 200);
});

it('should ensure callback signatures match', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -114,15 +107,12 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 200);
});
}), {});
assert(result.statusCode === 200);
});
it('should ensure callback signatures match - negative', async () => {

await handler(hydrateEvent({
it('should ensure callback signatures match - negative', async () => {
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -134,15 +124,12 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 500);
});
}), {});
assert(result.statusCode === 500);
});

it('should enforce signatures in callbacks', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -154,17 +141,13 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 500, "should return the file id");
assert(result.body.includes("invalid signature"));
});
}), {});
assert(result.statusCode === 500, "should return the file id");
assert(result.body.includes("invalid signature"));
});

it('should handle api gateway proxy callbacks', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -176,15 +159,12 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 200);
});
}), {});
assert(result.statusCode === 200);
});

it('should handle api gateway proxy callbacks and findings', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"id": "2e4612793298b1d691202e75dc125f6e",
"checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051",
"content_length": "1251174",
Expand All @@ -196,26 +176,21 @@ describe('Api Gateway handler tests', () => {
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 200);
});
}), {});
assert(result.statusCode === 200);
});

it('should handle api gateway callbacks with errors', async () => {
await handler(hydrateEvent({
const result = await handler(hydrateEvent({
"error": "error message",
"id": "a62a6f0ba82f6ac11e95d09b8bdf965c",
"metadata": {
"signature": utils.generateSignature("test-bucket", "test-key"),
"bucket": "test-bucket",
"key": "test-key"
}
}), {}, (error, result) => {
"use strict";
assert(error === null, "there should be no errors");
assert(result.statusCode === 200);
});
}), {});
assert(result.statusCode === 200);
});
});

Expand Down Expand Up @@ -283,4 +258,3 @@ const hydrateEvent = (body) => {
}
}
};

Loading
Loading