Skip to content

Commit 3c1a9d9

Browse files
authored
feat(astro): Add tracking of errors during HTML streaming (#15995)
Astro components are rendered in a stream, so if a component that is not the page fails during a request, this error is not thrown by the `await next()` call, only when the component is reached in the response stream. The code was iterating over the response stream assuming it was infallible, so component errors were being ignored by sentry and hidden from the user. This PR wraps the iteration on a `try/catch` pair to report the error and forward it along to the error handling pipeline.
1 parent 7e4a91e commit 3c1a9d9

File tree

2 files changed

+54
-5
lines changed

2 files changed

+54
-5
lines changed

packages/astro/src/server/middleware.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,18 @@ async function instrumentRequest(
184184

185185
const newResponseStream = new ReadableStream({
186186
start: async controller => {
187-
for await (const chunk of originalBody) {
188-
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
189-
const modifiedHtml = addMetaTagToHead(html);
190-
controller.enqueue(new TextEncoder().encode(modifiedHtml));
187+
try {
188+
for await (const chunk of originalBody) {
189+
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
190+
const modifiedHtml = addMetaTagToHead(html);
191+
controller.enqueue(new TextEncoder().encode(modifiedHtml));
192+
}
193+
} catch (e) {
194+
sendErrorToSentry(e);
195+
controller.error(e);
196+
} finally {
197+
controller.close();
191198
}
192-
controller.close();
193199
},
194200
});
195201

packages/astro/test/server/middleware.test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,49 @@ describe('sentryMiddleware', () => {
149149
});
150150
});
151151

152+
it('throws and sends an error to sentry if response streaming throws', async () => {
153+
const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException');
154+
155+
const middleware = handleRequest();
156+
const ctx = {
157+
request: {
158+
method: 'GET',
159+
url: '/users',
160+
headers: new Headers(),
161+
},
162+
url: new URL('https://myDomain.io/users/'),
163+
params: {},
164+
};
165+
166+
const error = new Error('Something went wrong');
167+
168+
const faultyStream = new ReadableStream({
169+
pull: controller => {
170+
controller.error(error);
171+
controller.close();
172+
},
173+
});
174+
175+
const next = vi.fn(() =>
176+
Promise.resolve(
177+
new Response(faultyStream, {
178+
headers: new Headers({ 'content-type': 'text/html' }),
179+
}),
180+
),
181+
);
182+
183+
// @ts-expect-error, a partial ctx object is fine here
184+
const resultFromNext = await middleware(ctx, next);
185+
186+
expect(resultFromNext?.headers.get('content-type')).toEqual('text/html');
187+
188+
await expect(() => resultFromNext!.text()).rejects.toThrowError();
189+
190+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
191+
mechanism: { handled: false, type: 'astro', data: { function: 'astroMiddleware' } },
192+
});
193+
});
194+
152195
describe('track client IP address', () => {
153196
it('attaches client IP if `trackClientIp=true` when handling dynamic page requests', async () => {
154197
const middleware = handleRequest({ trackClientIp: true });

0 commit comments

Comments
 (0)