Skip to content

Commit aa51022

Browse files
committed
fix(react-router): Avoid the TransformStream constructor in injectTraceMetaTags
workerd only supports a custom TransformStream transformer from compatibility date 2024-11-11, and Hydrogen's MiniOxygen runs with an older one, so the constructor threw and no trace meta tags were injected. Use a ReadableStream with a pull handler instead, as before, while keeping the shared </head> handling.
1 parent 2d4e891 commit aa51022

2 files changed

Lines changed: 69 additions & 16 deletions

File tree

‎packages/react-router/src/server/injectTraceMetaTags.ts‎

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,46 @@ export function injectTraceMetaTags(body: ReadableStream): ReadableStream {
1313
return body;
1414
}
1515

16+
// Resolved now, while the request's async context is still active. Runtimes like workerd run
17+
// stream callbacks outside of it, where there is no client and no trace data.
18+
const traceMetaTags = getTraceMetaTags();
19+
const injector = createHeadTagInjector(() => traceMetaTags);
20+
21+
const reader = body.getReader();
1622
const encoder = new TextEncoder();
1723
// A single streaming decoder carries incomplete multi-byte sequences across chunk
1824
// boundaries. A fresh, non-streaming decoder per chunk would flush a split character
1925
// as U+FFFD, corrupting the response (see https://github.com/whatwg/encoding/issues/184).
2026
const decoder = new TextDecoder();
21-
// Resolved now, while the request's async context is still active. Runtimes like workerd run
22-
// stream transforms outside of it, where there is no client and no trace data.
23-
const traceMetaTags = getTraceMetaTags();
24-
const injector = createHeadTagInjector(() => traceMetaTags);
2527

26-
return body.pipeThrough(
27-
new TransformStream<unknown, Uint8Array>({
28-
transform(value, controller) {
28+
// Not a TransformStream: workerd only supports a custom transformer from compatibility date
29+
// 2024-11-11, and Hydrogen's MiniOxygen runs with an older one.
30+
return new ReadableStream({
31+
async pull(controller) {
32+
// `pull` is not called again until a chunk is enqueued, so keep reading while text is held back.
33+
for (;;) {
34+
const { done, value } = await reader.read();
35+
36+
if (done) {
37+
const rest = injector.push(decoder.decode()) + injector.flush();
38+
if (rest) {
39+
controller.enqueue(encoder.encode(rest));
40+
}
41+
controller.close();
42+
return;
43+
}
44+
2945
const html = value instanceof Uint8Array ? decoder.decode(value, { stream: true }) : String(value);
3046
const out = injector.push(html);
47+
3148
if (out) {
3249
controller.enqueue(encoder.encode(out));
50+
return;
3351
}
34-
},
35-
flush(controller) {
36-
const out = injector.push(decoder.decode()) + injector.flush();
37-
if (out) {
38-
controller.enqueue(encoder.encode(out));
39-
}
40-
},
41-
}),
42-
);
52+
}
53+
},
54+
cancel(reason) {
55+
return reader.cancel(reason);
56+
},
57+
});
4358
}

‎packages/react-router/test/cloudflare/injectTraceMetaTags.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,44 @@ describe('injectTraceMetaTags', () => {
131131
expect(output).toBe('<html><body></he');
132132
});
133133

134+
test('does not rely on the TransformStream constructor, which older workerd compatibility dates reject', async () => {
135+
vi.stubGlobal(
136+
'TransformStream',
137+
class {
138+
constructor() {
139+
throw new Error('TransformStream is not supported');
140+
}
141+
},
142+
);
143+
144+
try {
145+
const input = streamFromChunks([new TextEncoder().encode('<html><head></head></html>')]);
146+
const output = new TextDecoder().decode(await readAll(injectTraceMetaTags(input)));
147+
148+
expect(output).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
149+
} finally {
150+
vi.unstubAllGlobals();
151+
}
152+
});
153+
154+
test('keeps reading while a partial tag is held back so the stream does not stall', async () => {
155+
const encoder = new TextEncoder();
156+
const input = streamFromChunks([encoder.encode('<head><'), encoder.encode('/'), encoder.encode('head><body>')]);
157+
158+
const output = new TextDecoder().decode(await readAll(injectTraceMetaTags(input)));
159+
160+
expect(output).toBe('<head><meta name="sentry-trace" content="test-trace-id"></head><body>');
161+
});
162+
163+
test('cancels the source stream when the consumer cancels', async () => {
164+
const cancel = vi.fn();
165+
const input = new ReadableStream({ cancel });
166+
167+
await injectTraceMetaTags(input).cancel('done');
168+
169+
expect(cancel).toHaveBeenCalledWith('done');
170+
});
171+
134172
test('resolves the trace meta tags synchronously, before the stream is read', async () => {
135173
const encoder = new TextEncoder();
136174
const input = streamFromChunks([encoder.encode('<html><head></head></html>')]);

0 commit comments

Comments
 (0)