Skip to content

Commit 78978b3

Browse files
committed
feat: w3c, return a frozen W3CFields from w3c() instead of a per-call clone
1 parent 9af6257 commit 78978b3

6 files changed

Lines changed: 42 additions & 23 deletions

File tree

‎src/context/constants.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const W3C_ALLOWED_FIELDS = [
3131
] as const;
3232

3333
export type W3CFieldName = (typeof W3C_ALLOWED_FIELDS)[number];
34+
export type W3CFields = Readonly<Partial<Record<W3CFieldName, string>>>;
3435

3536
// This RIC is used by Nodejs24 and above, it's used by NOdejs22 only for LMI and not OD
3637
export const CALLBACK_ERROR_NODEJS22 =

‎src/context/context-builder.test.ts‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ describe("ContextBuilder", () => {
339339
expect(context.clientContext).not.toHaveProperty("w3c");
340340
});
341341

342-
it("should return a fresh copy so callers cannot mutate the underlying map", () => {
342+
it("should expose a frozen object so callers cannot mutate the fields", () => {
343343
// GIVEN
344344
const headers = {
345345
...mockValidHeaders,
@@ -350,11 +350,22 @@ describe("ContextBuilder", () => {
350350

351351
// WHEN
352352
const context = ContextBuilder.build(headers);
353-
const first = context.w3c();
354-
first["baggage"] = "tampered";
355-
first["injected"] = "nope";
356353

357-
// THEN
354+
// THEN — the object is frozen
355+
expect(Object.isFrozen(context.w3c())).toBe(true);
356+
357+
// AND — attempts to write silently no-op in sloppy mode and throw in
358+
// strict mode. The test file is a strict ESM TypeScript module, so
359+
// both overwriting an existing key and adding a new one throw.
360+
const mutable = context.w3c() as Record<string, string>;
361+
expect(() => {
362+
mutable["baggage"] = "tampered";
363+
}).toThrow(TypeError);
364+
expect(() => {
365+
mutable["injected"] = "nope";
366+
}).toThrow(TypeError);
367+
368+
// AND — the value is unchanged.
358369
expect(context.w3c()).toEqual({ baggage: "abc" });
359370
});
360371

‎src/context/context-builder.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
REQUIRED_ENV_VARS,
55
REQUIRED_INVOKE_HEADERS,
66
W3C_ALLOWED_FIELDS,
7+
W3CFields,
78
W3CFieldName,
89
} from "./constants.js";
910
import { InvokeContext, InvokeHeaders } from "./types.js";
@@ -54,33 +55,33 @@ export class ContextBuilder {
5455
getRemainingTimeInMillis: function () {
5556
return deadline - Date.now();
5657
},
57-
w3c: function (): Partial<Record<W3CFieldName, string>> {
58-
return { ...w3cFields };
58+
w3c: function (): W3CFields {
59+
return w3cFields;
5960
},
6061
};
6162
}
6263

6364
/**
64-
* Pulls `w3c` out of the parsed `clientContext` and returns a normalized
65-
* copy of the allowlisted string fields (see `W3C_ALLOWED_FIELDS`). The
66-
* `w3c` key is removed from `clientContext` itself so callers cannot read
67-
* the source through `context.clientContext`.
65+
* Pulls `w3c` out of the parsed `clientContext` and returns a frozen,
66+
* normalized copy of the allowlisted string fields (see
67+
* `W3C_ALLOWED_FIELDS`). The `w3c` key is removed from `clientContext`
68+
* itself so callers cannot read the source through `context.clientContext`.
6869
*/
6970
private static extractAndStripW3c(
7071
clientContext: Record<string, unknown> | undefined,
71-
): Partial<Record<W3CFieldName, string>> {
72+
): W3CFields {
7273
if (!clientContext || typeof clientContext !== "object") {
73-
return {};
74+
return Object.freeze({});
7475
}
7576
if (!("w3c" in clientContext)) {
76-
return {};
77+
return Object.freeze({});
7778
}
7879

7980
const rawW3c = clientContext.w3c;
8081
delete clientContext.w3c;
8182

8283
if (!rawW3c || typeof rawW3c !== "object" || Array.isArray(rawW3c)) {
83-
return {};
84+
return Object.freeze({});
8485
}
8586

8687
const source = rawW3c as Record<string, unknown>;
@@ -91,7 +92,7 @@ export class ContextBuilder {
9192
fields[key] = value;
9293
}
9394
}
94-
return fields;
95+
return Object.freeze(fields);
9596
}
9697

9798
private static parseDeadline(invokeHeaders: InvokeHeaders) {

‎src/context/types.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WritableResponseStream } from "../stream/index.js";
22
import {
33
OPTIONAL_INVOKE_HEADERS,
44
REQUIRED_INVOKE_HEADERS,
5-
W3CFieldName,
5+
W3CFields,
66
} from "./constants.js";
77

88
export interface InvokeHeaders {
@@ -41,7 +41,7 @@ export interface InvokeContext {
4141
* Returns the W3C trace context fields (traceparent, tracestate, baggage)
4242
* that were carried on `clientContext.w3c` at invoke time.
4343
*/
44-
w3c(): Partial<Record<W3CFieldName, string>>;
44+
w3c(): W3CFields;
4545
}
4646

4747
export interface StreamOptions {

‎test/dockerized/suites/w3c.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"tests": [
33
{
4-
"name": "w3c_is_a_function_on_context",
5-
"handler": "w3c.w3cIsFunction",
4+
"name": "w3c_is_a_function_returning_a_frozen_object",
5+
"handler": "w3c.w3cShape",
66
"request": {},
77
"assertions": [
8-
{ "response": { "isFunction": true } }
8+
{ "response": { "typeofW3c": "function", "typeofResult": "object", "isFrozen": true, "isObject": true } }
99
]
1010
},
1111

‎test/dockerized/tasks/w3c.mjs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export const echoClientContext = async (_event, context) => {
2020
return context.clientContext ?? null;
2121
};
2222

23-
export const w3cIsFunction = async (_event, context) => {
24-
return { isFunction: typeof context.w3c === "function" };
23+
export const w3cShape = async (_event, context) => {
24+
const value = context.w3c();
25+
return {
26+
typeofW3c: typeof context.w3c,
27+
typeofResult: typeof value,
28+
isFrozen: Object.isFrozen(value),
29+
isObject: value !== null && typeof value === "object",
30+
};
2531
};

0 commit comments

Comments
 (0)