Skip to content

Commit d80e7c8

Browse files
feat(j0di3): Forward Idempotency-Key and X-Request-ID through proxy (#1145)
Mutating J0dI3 endpoints accept an Idempotency-Key for safe retries and echo X-Request-ID for support correlation. The shared dispatch() now forwards both request headers when present and surfaces the echoed X-Request-ID on the proxied response.
1 parent 6afa18d commit d80e7c8

2 files changed

Lines changed: 66 additions & 7 deletions

File tree

__tests__/lib/j0di3/j0di3-proxy.test.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ vi.mock("axios", () => ({
1515
},
1616
}));
1717

18-
import j0di3 from "@/lib/j0di3-client";
1918
import axios from "axios";
19+
import j0di3 from "@/lib/j0di3-client";
2020
import { j0di3Proxy } from "@/lib/j0di3-proxy";
2121

2222
const mockJ0di3 = j0di3 as unknown as Mock;
2323
const mockIsAxiosError = axios.isAxiosError as Mock;
2424

25-
function createMockReqRes(
26-
overrides: Partial<NextApiRequest> = {}
27-
): { req: NextApiRequest; res: NextApiResponse } {
25+
function createMockReqRes(overrides: Partial<NextApiRequest> = {}): {
26+
req: NextApiRequest;
27+
res: NextApiResponse;
28+
} {
2829
const req = {
2930
method: "POST",
3031
body: { question: "What is React?" },
@@ -43,6 +44,7 @@ function createMockReqRes(
4344
const res = {
4445
status: vi.fn().mockReturnThis(),
4546
json: vi.fn().mockReturnThis(),
47+
setHeader: vi.fn().mockReturnThis(),
4648
} as unknown as NextApiResponse;
4749

4850
return { req, res };
@@ -100,6 +102,44 @@ describe("j0di3Proxy", () => {
100102
);
101103
});
102104

105+
it("forwards Idempotency-Key and X-Request-ID headers when supplied", async () => {
106+
mockJ0di3.mockResolvedValue({ data: { ok: true } });
107+
108+
const handler = j0di3Proxy("POST", "/api/v1/challenges/c-1/submit");
109+
const { req, res } = createMockReqRes({
110+
headers: {
111+
"idempotency-key": "idem-key-789",
112+
"x-request-id": "req-id-456",
113+
} as unknown as NextApiRequest["headers"],
114+
});
115+
116+
await handler(req, res);
117+
118+
expect(mockJ0di3).toHaveBeenCalledWith(
119+
expect.objectContaining({
120+
headers: {
121+
"X-Troop-Token": "troop-token-abc",
122+
"Idempotency-Key": "idem-key-789",
123+
"X-Request-ID": "req-id-456",
124+
},
125+
})
126+
);
127+
});
128+
129+
it("echoes the J0dI3 X-Request-ID back on the response", async () => {
130+
mockJ0di3.mockResolvedValue({
131+
data: { ok: true },
132+
headers: { "x-request-id": "echoed-req-id" },
133+
});
134+
135+
const handler = j0di3Proxy("POST", "/api/v1/challenges/c-1/submit");
136+
const { req, res } = createMockReqRes();
137+
138+
await handler(req, res);
139+
140+
expect(res.setHeader).toHaveBeenCalledWith("X-Request-ID", "echoed-req-id");
141+
});
142+
103143
it("returns 400 when user has no troopId", async () => {
104144
const handler = j0di3Proxy("POST", "/api/v1/learning/explain");
105145
const { req, res } = createMockReqRes();

src/lib/j0di3-proxy.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,29 @@ async function dispatch(
4646
: req.query
4747
: undefined;
4848

49-
const headers = injectTroopId && troopToken ? { "X-Troop-Token": troopToken } : undefined;
49+
const headers: Record<string, string> = {};
50+
if (injectTroopId && troopToken) {
51+
headers["X-Troop-Token"] = troopToken;
52+
}
53+
// Forward idempotency + correlation headers when the caller supplies them.
54+
const idempotencyKey = req.headers?.["idempotency-key"];
55+
if (typeof idempotencyKey === "string") {
56+
headers["Idempotency-Key"] = idempotencyKey;
57+
}
58+
const requestId = req.headers?.["x-request-id"];
59+
if (typeof requestId === "string") {
60+
headers["X-Request-ID"] = requestId;
61+
}
62+
63+
const response = await j0di3({ method, url, data: body, params, headers });
64+
65+
// J0dI3 echoes X-Request-ID back; surface it for support correlation.
66+
const echoedRequestId = response.headers?.["x-request-id"];
67+
if (typeof echoedRequestId === "string") {
68+
res.setHeader("X-Request-ID", echoedRequestId);
69+
}
5070

51-
const { data } = await j0di3({ method, url, data: body, params, headers });
52-
res.json(data);
71+
res.json(response.data);
5372
} catch (error: unknown) {
5473
if (axios.isAxiosError(error) && error.response) {
5574
const status = error.response.status;

0 commit comments

Comments
 (0)