diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts index 3b9554fdda73..59ce4e6f36a8 100644 --- a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts +++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts @@ -77,11 +77,24 @@ function injectMetaTagsInResponse(originalResponse: Response): Response { } } + // Once we've injected, pass later chunks through untouched so a
inside a + // quoted attribute in a later chunk isn't matched again. + let injected = false; + let errored = false; try { for await (const chunk of bodyReporter()) { const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); + + if (injected) { + controller.enqueue(new TextEncoder().encode(html)); + continue; + } + const modifiedHtml = addMetaTagToHead(html, metaTagsStr); + if (modifiedHtml !== html) { + injected = true; + } controller.enqueue(new TextEncoder().encode(modifiedHtml)); } } catch (e) { diff --git a/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts b/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts index 5928cefeb9b7..ba5991d6fb3a 100644 --- a/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts +++ b/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts @@ -165,6 +165,31 @@ describe('wrapFetchWithSentry', () => { expect(html).toContain('data-content="ignore"'); }); + it('injects meta tags only once when a later chunk has inside a quoted attribute', async () => { + const encoder = new TextEncoder(); + const chunks = ['']; + const body = new ReadableStream({ + start(controller) { + for (const c of chunks) controller.enqueue(encoder.encode(c)); + controller.close(); + }, + }); + const mockResponse = new Response(body, { + headers: new Headers({ 'content-type': 'text/html' }), + }); + const fetchFn = vi.fn().mockResolvedValue(mockResponse); + + const serverEntry = wrapFetchWithSentry({ fetch: fetchFn }); + const request = new Request('http://localhost:3000/'); + + const response = await serverEntry.fetch(request); + const html = await response.text(); + + expect(html.match(/name="sentry-trace"/g)).toHaveLength(1); + expect(html).toContain('junk"'); + }); + it('captures exception when HTML response body stream errors', async () => { const streamError = new Error('stream read error'); const body = new ReadableStream({