mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 22:55:13 +00:00
feat: Add request timeout handling to sendMessageStream and throw ETIMEDOUT on timeout.
This commit is contained in:
@@ -1012,6 +1012,57 @@ describe('GeminiChat', () => {
|
||||
'prompt-id-thinking-budget',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ETIMEDOUT when request times out', async () => {
|
||||
// 1. Mock AbortSignal.timeout to return a manually controllable signal
|
||||
const manualTimeoutController = new AbortController();
|
||||
const timeoutSpy = vi
|
||||
.spyOn(AbortSignal, 'timeout')
|
||||
.mockReturnValue(manualTimeoutController.signal);
|
||||
|
||||
// 2. Mock generateContentStream to hang UNTIL aborted
|
||||
vi.mocked(mockContentGenerator.generateContentStream).mockImplementation(
|
||||
(request) => new Promise((resolve, reject) => {
|
||||
const config = request?.config;
|
||||
if (config?.abortSignal) {
|
||||
if (config.abortSignal.aborted) {
|
||||
reject(new Error('Aborted'));
|
||||
return;
|
||||
}
|
||||
config.abortSignal.addEventListener('abort', () => {
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. Start the request
|
||||
const streamPromise = chat.sendMessageStream(
|
||||
{ model: 'gemini-2.0-flash' },
|
||||
'test timeout',
|
||||
'prompt-id-timeout',
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
// 4. Simulate timeout by aborting the signal
|
||||
// We need to wait a tick to ensure the promise starts executing
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
manualTimeoutController.abort();
|
||||
|
||||
// 5. Assert it throws ETIMEDOUT
|
||||
await expect(async () => {
|
||||
for await (const _ of await streamPromise) {
|
||||
// consume
|
||||
}
|
||||
}).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Request timed out'),
|
||||
code: 'ETIMEDOUT',
|
||||
}),
|
||||
);
|
||||
|
||||
timeoutSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addHistory', () => {
|
||||
|
||||
@@ -87,6 +87,12 @@ const INVALID_CONTENT_RETRY_OPTIONS: ContentRetryOptions = {
|
||||
|
||||
export const SYNTHETIC_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
||||
|
||||
/**
|
||||
* Timeout for API calls in milliseconds.
|
||||
* Defaulting to 60 seconds (configurable in future).
|
||||
*/
|
||||
const TIMEOUT_MS = 60000;
|
||||
|
||||
/**
|
||||
* Returns true if the response is valid, false otherwise.
|
||||
*/
|
||||
@@ -505,13 +511,18 @@ export class GeminiChat {
|
||||
}
|
||||
|
||||
lastModelToUse = modelToUse;
|
||||
|
||||
// Create a timeout signal to prevent long hanging requests.
|
||||
const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS);
|
||||
const combinedSignal = AbortSignal.any([abortSignal, timeoutSignal]);
|
||||
|
||||
const config: GenerateContentConfig = {
|
||||
...currentGenerateContentConfig,
|
||||
// TODO(12622): Ensure we don't overrwrite these when they are
|
||||
// passed via config.
|
||||
systemInstruction: this.systemInstruction,
|
||||
tools: this.tools,
|
||||
abortSignal,
|
||||
abortSignal: combinedSignal,
|
||||
};
|
||||
|
||||
let contentsToUse = isPreviewModel(modelToUse)
|
||||
@@ -580,14 +591,25 @@ export class GeminiChat {
|
||||
lastConfig = config;
|
||||
lastContentsToUse = contentsToUse;
|
||||
|
||||
return this.config.getContentGenerator().generateContentStream(
|
||||
{
|
||||
model: modelToUse,
|
||||
contents: contentsToUse,
|
||||
config,
|
||||
},
|
||||
prompt_id,
|
||||
);
|
||||
try {
|
||||
return await this.config.getContentGenerator().generateContentStream(
|
||||
{
|
||||
model: modelToUse,
|
||||
contents: contentsToUse,
|
||||
config,
|
||||
},
|
||||
prompt_id,
|
||||
);
|
||||
} catch (error) {
|
||||
if (timeoutSignal.aborted) {
|
||||
const timeoutError = new Error(
|
||||
`Request timed out after ${TIMEOUT_MS}ms`,
|
||||
);
|
||||
(timeoutError as unknown as { code: string }).code = 'ETIMEDOUT';
|
||||
throw timeoutError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const onPersistent429Callback = async (
|
||||
|
||||
Reference in New Issue
Block a user