fix(core): retry on ERR_STREAM_PREMATURE_CLOSE errors (#26519)

This commit is contained in:
Coco Sheng
2026-05-05 15:19:50 -04:00
committed by GitHub
parent e80d7cc083
commit f5c0977e96
2 changed files with 63 additions and 0 deletions
@@ -587,4 +587,66 @@ describe('GeminiChat Network Retries', () => {
}),
);
});
it('should retry on premature stream closure (ERR_STREAM_PREMATURE_CLOSE)', async () => {
mockConfig.getRetryFetchErrors = vi.fn().mockReturnValue(true);
const prematureCloseError = new Error('Premature close');
Object.defineProperty(prematureCloseError, 'code', {
value: 'ERR_STREAM_PREMATURE_CLOSE',
});
vi.mocked(mockContentGenerator.generateContentStream)
.mockResolvedValueOnce(
(async function* () {
yield {
candidates: [{ content: { parts: [{ text: 'Incomplete part' }] } }],
} as unknown as GenerateContentResponse;
throw prematureCloseError;
})(),
)
.mockResolvedValueOnce(
(async function* () {
yield {
candidates: [
{
content: { parts: [{ text: 'Complete response after retry' }] },
finishReason: 'STOP',
},
],
} as unknown as GenerateContentResponse;
})(),
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-premature-close',
new AbortController().signal,
LlmRole.MAIN,
);
const events: StreamEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
expect(retryEvent).toBeDefined();
const successChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Complete response after retry',
);
expect(successChunk).toBeDefined();
expect(mockLogNetworkRetryAttempt).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
error_type: 'ERR_STREAM_PREMATURE_CLOSE',
}),
);
});
});
+1
View File
@@ -58,6 +58,7 @@ const RETRYABLE_NETWORK_CODES = [
'UND_ERR_HEADERS_TIMEOUT',
'UND_ERR_BODY_TIMEOUT',
'UND_ERR_CONNECT_TIMEOUT',
'ERR_STREAM_PREMATURE_CLOSE',
];
// Node.js builds SSL error codes by prepending ERR_SSL_ to the uppercased