Skip to content

Commit 4d1ff38

Browse files
authored
Setup API lambda response streaming (#249)
1 parent 8d3c93e commit 4d1ff38

File tree

9 files changed

+260
-125
lines changed

9 files changed

+260
-125
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,4 @@ __pycache__
144144
dist_devel/
145145
!src/ui/pages/logs
146146
src/api/package.lambda.json
147+
tfplan

eslint.config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
77
import js from "@eslint/js";
88
import { FlatCompat } from "@eslint/eslintrc";
99
import mantine from "eslint-config-mantine";
10+
import globals from 'globals';
1011

1112
const __filename = fileURLToPath(import.meta.url);
1213
const __dirname = path.dirname(__filename);
@@ -74,6 +75,15 @@ export default defineConfig([
7475
files: ["src/api/build.js"],
7576
rules: { "import/extensions": "off" },
7677
},
78+
{
79+
files: ["src/api/lambda.ts"],
80+
languageOptions: {
81+
globals: {
82+
...globals.node,
83+
'awslambda': 'readonly'
84+
}
85+
}
86+
},
7787
{
7888
files: ["src/ui/*", "src/ui/**/*"],
7989
rules: {

src/api/lambda.ts

Lines changed: 58 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,94 @@
11
import awsLambdaFastify from "@fastify/aws-lambda";
2+
import { pipeline } from "node:stream/promises";
23
import init, { instanceId } from "./index.js";
3-
import { type APIGatewayEvent, type Context } from "aws-lambda";
44
import { InternalServerError, ValidationError } from "common/errors/index.js";
5+
import { Readable } from "node:stream";
56

7+
// Initialize the proxy with the payloadAsStream option
68
const app = await init();
7-
const realHandler = awsLambdaFastify(app, {
9+
const proxy = awsLambdaFastify(app, {
10+
payloadAsStream: true,
811
decorateRequest: false,
9-
serializeLambdaArguments: true,
1012
callbackWaitsForEmptyEventLoop: false,
11-
binaryMimeTypes: ["application/octet-stream", "application/vnd.apple.pkpass"],
13+
serializeLambdaArguments: true,
14+
binaryMimeTypes: ["application/octet-stream", "application/vnd.apple.pkpass"], // from original code
1215
});
1316

14-
type WarmerEvent = { action: "warmer" };
15-
16-
/**
17-
* Validates the origin verification header against the current and previous keys.
18-
* @returns {boolean} `true` if the request is valid, otherwise `false`.
19-
*/
2017
const validateOriginHeader = (
21-
originHeader: string | undefined,
18+
originHeader: string,
2219
currentKey: string,
2320
previousKey: string | undefined,
2421
previousKeyExpiresAt: string | undefined,
25-
): boolean => {
26-
// 1. A header must exist to be valid.
22+
) => {
2723
if (!originHeader) {
2824
return false;
2925
}
30-
31-
// 2. Check against the current key first for an early return on the happy path.
3226
if (originHeader === currentKey) {
3327
return true;
3428
}
35-
36-
// 3. If it's not the current key, check the previous key during the rotation window.
3729
if (previousKey && previousKeyExpiresAt) {
3830
const isExpired = new Date() >= new Date(previousKeyExpiresAt);
3931
if (originHeader === previousKey && !isExpired) {
4032
return true;
4133
}
4234
}
43-
44-
// 4. If all checks fail, the header is invalid.
4535
return false;
4636
};
4737

48-
const handler = async (
49-
event: APIGatewayEvent | WarmerEvent,
50-
context: Context,
51-
) => {
52-
if ("action" in event && event.action === "warmer") {
53-
return { instanceId };
54-
}
55-
event = event as APIGatewayEvent;
38+
// This handler now correctly uses the native streaming support from the packages.
39+
export const handler = awslambda.streamifyResponse(
40+
async (event: any, responseStream: any, context: any) => {
41+
context.callbackWaitsForEmptyEventLoop = false;
42+
if ("action" in event && event.action === "warmer") {
43+
const requestStream = Readable.from(
44+
Buffer.from(JSON.stringify({ instanceId })),
45+
);
46+
await pipeline(requestStream, responseStream);
47+
return;
48+
}
5649

57-
const currentKey = process.env.ORIGIN_VERIFY_KEY;
58-
const previousKey = process.env.PREVIOUS_ORIGIN_VERIFY_KEY;
59-
const previousKeyExpiresAt =
60-
process.env.PREVIOUS_ORIGIN_VERIFY_KEY_EXPIRES_AT;
50+
// 2. Perform origin header validation before calling the proxy
51+
const currentKey = process.env.ORIGIN_VERIFY_KEY;
52+
if (currentKey) {
53+
const previousKey = process.env.PREVIOUS_ORIGIN_VERIFY_KEY;
54+
const previousKeyExpiresAt =
55+
process.env.PREVIOUS_ORIGIN_VERIFY_KEY_EXPIRES_AT;
6156

62-
// Log an error if the previous key has expired but is still configured.
63-
if (previousKey && previousKeyExpiresAt) {
64-
if (new Date() >= new Date(previousKeyExpiresAt)) {
65-
console.error(
66-
"Expired previous origin verify key is still present in the environment. Expired at:",
57+
const isValid = validateOriginHeader(
58+
event.headers?.["x-origin-verify"],
59+
currentKey,
60+
previousKey,
6761
previousKeyExpiresAt,
6862
);
69-
}
70-
}
7163

72-
// Proceed with verification only if a current key is set.
73-
if (currentKey) {
74-
const isValid = validateOriginHeader(
75-
event.headers?.["x-origin-verify"],
76-
currentKey,
77-
previousKey,
78-
previousKeyExpiresAt,
79-
);
64+
if (!isValid) {
65+
const error = new ValidationError({ message: "Request is not valid." });
66+
const body = JSON.stringify(error.toJson());
8067

81-
if (!isValid) {
82-
const newError = new ValidationError({
83-
message: "Request is not valid.",
84-
});
85-
const json = JSON.stringify(newError.toJson());
86-
return {
87-
statusCode: newError.httpStatusCode,
88-
body: json,
89-
headers: {
90-
"Content-Type": "application/json",
91-
},
92-
isBase64Encoded: false,
93-
};
68+
// On validation failure, manually create the response
69+
const meta = {
70+
statusCode: error.httpStatusCode,
71+
headers: { "Content-Type": "application/json" },
72+
};
73+
responseStream = awslambda.HttpResponseStream.from(
74+
responseStream,
75+
meta,
76+
);
77+
const requestStream = Readable.from(Buffer.from(body));
78+
await pipeline(requestStream, responseStream);
79+
return;
80+
}
81+
delete event.headers["x-origin-verify"];
9482
}
9583

96-
delete event.headers["x-origin-verify"];
97-
}
98-
99-
// If verification is disabled or passed, proceed with the real handler logic.
100-
return await realHandler(event, context).catch((e) => {
101-
console.error(e);
102-
const newError = new InternalServerError({
103-
message: "Failed to initialize application.",
104-
});
105-
const json = JSON.stringify(newError.toJson());
106-
return {
107-
statusCode: newError.httpStatusCode,
108-
body: json,
109-
headers: {
110-
"Content-Type": "application/json",
111-
},
112-
isBase64Encoded: false,
113-
};
114-
});
115-
};
116-
117-
await app.ready();
118-
export { handler };
84+
const { stream, meta } = await proxy(event, context);
85+
// Fix issue with Lambda where streaming repsonses always require a body to be present
86+
const body =
87+
stream.readableLength > 0 ? stream : Readable.from(Buffer.from(" "));
88+
responseStream = awslambda.HttpResponseStream.from(
89+
responseStream,
90+
meta as any,
91+
);
92+
await pipeline(body, responseStream);
93+
},
94+
);

0 commit comments

Comments
 (0)