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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(shared):** share request context stores across duplicated modules ([#15395](https://github.com/diegosouzapw/OmniRoute/pull/15395)) — thanks @maxmad64bis
14 changes: 11 additions & 3 deletions src/shared/middleware/correlationId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
import { AsyncLocalStorage } from "node:async_hooks";
import crypto from "crypto";

const correlationStore = new AsyncLocalStorage();
const CORRELATION_STORE_KEY = Symbol.for("omniroute.correlationId.store");
type GlobalWithCorrelationStore = typeof globalThis & {
[CORRELATION_STORE_KEY]?: AsyncLocalStorage<string | undefined>;
};

function getCorrelationStore(): AsyncLocalStorage<string | undefined> {
return ((globalThis as GlobalWithCorrelationStore)[CORRELATION_STORE_KEY] ??=
new AsyncLocalStorage<string | undefined>());
}

/**
* Generate a unique correlation ID.
Expand All @@ -26,7 +34,7 @@ function generateCorrelationId() {
* @returns {string|undefined}
*/
export function getCorrelationId() {
return correlationStore.getStore();
return getCorrelationStore().getStore();
}

/**
Expand All @@ -39,5 +47,5 @@ export function getCorrelationId() {
*/
export function runWithCorrelation(correlationId, fn) {
const id = correlationId || generateCorrelationId();
return correlationStore.run(id, fn);
return getCorrelationStore().run(id, fn);
}
14 changes: 11 additions & 3 deletions src/shared/utils/requestId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ type RequestLike = {
headers?: HeaderReader | null;
} | null;

const requestIdStore = new AsyncLocalStorage<string>();
const REQUEST_ID_STORE_KEY = Symbol.for("omniroute.requestId.store");
type GlobalWithRequestIdStore = typeof globalThis & {
[REQUEST_ID_STORE_KEY]?: AsyncLocalStorage<string>;
};

function getRequestIdStore(): AsyncLocalStorage<string> {
return ((globalThis as GlobalWithRequestIdStore)[REQUEST_ID_STORE_KEY] ??=
new AsyncLocalStorage<string>());
}

function getHeaderValue(request: RequestLike, name: string): string | null {
const value = request?.headers?.get?.(name);
Expand All @@ -35,7 +43,7 @@ function getHeaderValue(request: RequestLike, name: string): string | null {
* @returns {string | null}
*/
export function getRequestId() {
return requestIdStore.getStore() || null;
return getRequestIdStore().getStore() || null;
}

/**
Expand All @@ -54,7 +62,7 @@ export async function withRequestId<T>(
): Promise<T> {
const existingId = getHeaderValue(request, "x-request-id");
const requestId = existingId || randomUUID();
return requestIdStore.run(requestId, handler);
return getRequestIdStore().run(requestId, handler);
}

/**
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/share-request-context-symbol-for.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import test from "node:test";
import assert from "node:assert/strict";

// Two module copies share the request contexts: a read from copy A must see the
// context opened by copy B. This mirrors production, where the module is bundled
// once per server chunk, so middleware opening the context and the route reading
// it may run on different copies. Single-copy coverage alone cannot catch this:
// it passes before and after.

const CORRELATION_PATH = "../../src/shared/middleware/correlationId.ts";
const REQUEST_ID_PATH = "../../src/shared/utils/requestId.ts";

test("a read from copy A sees the correlation opened by copy B", async () => {
const copyA = await import(`${CORRELATION_PATH}?graph=a`);
const copyB = await import(`${CORRELATION_PATH}?graph=b`);
const id = "corr-shared-1";
const seen = copyB.runWithCorrelation(id, () => copyA.getCorrelationId());
assert.equal(seen, id);
});

test("a read from copy A sees the request id opened by copy B", async () => {
const copyA = await import(`${REQUEST_ID_PATH}?graph=a`);
const copyB = await import(`${REQUEST_ID_PATH}?graph=b`);
const id = "req-shared-1";
const seen = await copyB.withRequestId({ headers: { get: () => id } }, () =>
copyA.getRequestId()
);
assert.equal(seen, id);
});

test("sequential contexts keep their own id, outside yields the empty value", async () => {
const corrA = await import(`${CORRELATION_PATH}?graph=c`);
const corrB = await import(`${CORRELATION_PATH}?graph=d`);
assert.equal(
corrB.runWithCorrelation("corr-first", () => corrA.getCorrelationId()),
"corr-first"
);
assert.equal(
corrB.runWithCorrelation("corr-second", () => corrA.getCorrelationId()),
"corr-second"
);
assert.equal(corrA.getCorrelationId(), undefined);

const reqA = await import(`${REQUEST_ID_PATH}?graph=c`);
const reqB = await import(`${REQUEST_ID_PATH}?graph=d`);
const first = await reqB.withRequestId({ headers: { get: () => "req-first" } }, () =>
reqA.getRequestId()
);
assert.equal(first, "req-first");
const second = await reqB.withRequestId({ headers: { get: () => "req-second" } }, () =>
reqA.getRequestId()
);
assert.equal(second, "req-second");
assert.equal(reqA.getRequestId(), null);
});
Loading