🐛 fix(fetch-sse): stop injecting contextBody into structured provider errors (#13477)

* 🐛 fix(fetch-sse): stop injecting contextBody into structured provider errors

Structured errors (ProviderBizError etc.) already contain complete context.
Spreading contextBody into their body overwrites fields like `provider` and
pollutes the error structure that downstream renderers depend on.

Fixes #13476

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(fetch-sse): add regression test for structured error body pollution

Ensures structured provider errors (e.g. ProviderBizError) are passed through
unchanged without contextBody injection, and that contextBody is only applied
to unknown/unstructured errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu 2026-04-01 21:24:01 +08:00 committed by GitHub
parent 6ecae1bbd1
commit 8af28a778b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 3 deletions

View file

@ -598,6 +598,42 @@ describe('fetchSSE', () => {
expect(mockOnErrorHandle).toHaveBeenCalledWith(mockError);
});
it('should NOT inject contextBody into structured provider errors (regression)', async () => {
const mockOnErrorHandle = vi.fn();
const mockError: ChatMessageError = {
body: {
error: {
type: 'invalid_request_error',
message: 'Invalid signature in thinking block',
},
provider: 'lobehub',
errorType: 'ProviderBizError',
},
message: 'ProviderBizError',
type: 'ProviderBizError',
};
(fetchEventSource as any).mockImplementationOnce(
(url: string, options: FetchEventSourceInit) => {
options.onerror!(mockError);
},
);
try {
await fetchSSE('/', {
onErrorHandle: mockOnErrorHandle,
requestContext: { provider: 'openai', model: 'gpt-4o' },
});
} catch (e) {}
expect(mockOnErrorHandle).toHaveBeenCalledWith(mockError);
const receivedError = mockOnErrorHandle.mock.calls[0][0];
expect(receivedError.body).not.toHaveProperty('elapsedMs');
expect(receivedError.body).not.toHaveProperty('networkStatus');
expect(receivedError.body).not.toHaveProperty('model');
expect(receivedError.body.provider).toBe('lobehub');
});
it('should call onErrorHandle when Unknown error is thrown', async () => {
const mockOnErrorHandle = vi.fn();
const mockError = new Error('Unknown error');
@ -609,7 +645,10 @@ describe('fetchSSE', () => {
);
try {
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
await fetchSSE('/', {
onErrorHandle: mockOnErrorHandle,
requestContext: { provider: 'openai', model: 'gpt-4o' },
});
} catch (e) {}
expect(mockOnErrorHandle).toHaveBeenCalledWith({
@ -618,7 +657,10 @@ describe('fetchSSE', () => {
body: {
message: 'Unknown error',
name: 'Error',
stack: expect.any(String),
provider: 'openai',
model: 'gpt-4o',
elapsedMs: expect.any(Number),
networkStatus: expect.any(Boolean),
},
});
});

View file

@ -323,7 +323,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
options.onErrorHandle?.(
error.type
? { ...error, body: { ...error.body, ...contextBody } }
? error
: {
body: {
message: error.message,