Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fd64971

Browse files
authoredJan 17, 2024
chore(internal): debug logging for retries; speculative retry-after-ms support (#633)
1 parent e109d40 commit fd64971

File tree

2 files changed

+77
-14
lines changed

2 files changed

+77
-14
lines changed
 

‎src/core.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -417,14 +417,17 @@ export abstract class APIClient {
417417

418418
if (!response.ok) {
419419
if (retriesRemaining && this.shouldRetry(response)) {
420+
const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
421+
debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders);
420422
return this.retryRequest(options, retriesRemaining, responseHeaders);
421423
}
422424

423425
const errText = await response.text().catch((e) => castToError(e).message);
424426
const errJSON = safeJSON(errText);
425427
const errMessage = errJSON ? undefined : errText;
428+
const retryMessage = retriesRemaining ? `(error; no more retries left)` : `(error; not retryable)`;
426429

427-
debug('response', response.status, url, responseHeaders, errMessage);
430+
debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders, errMessage);
428431

429432
const err = this.makeStatusError(response.status, errJSON, errMessage, responseHeaders);
430433
throw err;
@@ -529,11 +532,21 @@ export abstract class APIClient {
529532
retriesRemaining: number,
530533
responseHeaders?: Headers | undefined,
531534
): Promise<APIResponseProps> {
532-
// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
533535
let timeoutMillis: number | undefined;
536+
537+
// Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
538+
const retryAfterMillisHeader = responseHeaders?.['retry-after-ms'];
539+
if (retryAfterMillisHeader) {
540+
const timeoutMs = parseFloat(retryAfterMillisHeader);
541+
if (!Number.isNaN(timeoutMs)) {
542+
timeoutMillis = timeoutMs;
543+
}
544+
}
545+
546+
// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
534547
const retryAfterHeader = responseHeaders?.['retry-after'];
535-
if (retryAfterHeader) {
536-
const timeoutSeconds = parseInt(retryAfterHeader);
548+
if (retryAfterHeader && !timeoutMillis) {
549+
const timeoutSeconds = parseFloat(retryAfterHeader);
537550
if (!Number.isNaN(timeoutSeconds)) {
538551
timeoutMillis = timeoutSeconds * 1000;
539552
} else {
@@ -543,12 +556,7 @@ export abstract class APIClient {
543556

544557
// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
545558
// just do what it says, but otherwise calculate a default
546-
if (
547-
!timeoutMillis ||
548-
!Number.isInteger(timeoutMillis) ||
549-
timeoutMillis <= 0 ||
550-
timeoutMillis > 60 * 1000
551-
) {
559+
if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) {
552560
const maxRetries = options.maxRetries ?? this.maxRetries;
553561
timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
554562
}

‎tests/index.test.ts

+59-4
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,18 @@ describe('request building', () => {
217217
});
218218

219219
describe('retries', () => {
220-
test('single retry', async () => {
220+
test('retry on timeout', async () => {
221221
let count = 0;
222222
const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise<Response> => {
223-
if (!count++)
223+
if (count++ === 0) {
224224
return new Promise(
225225
(resolve, reject) => signal?.addEventListener('abort', () => reject(new Error('timed out'))),
226226
);
227+
}
227228
return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
228229
};
229230

230-
const client = new OpenAI({ apiKey: 'My API Key', timeout: 2000, fetch: testFetch });
231+
const client = new OpenAI({ apiKey: 'My API Key', timeout: 10, fetch: testFetch });
231232

232233
expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
233234
expect(count).toEqual(2);
@@ -238,5 +239,59 @@ describe('retries', () => {
238239
.then((r) => r.text()),
239240
).toEqual(JSON.stringify({ a: 1 }));
240241
expect(count).toEqual(3);
241-
}, 10000);
242+
});
243+
244+
test('retry on 429 with retry-after', async () => {
245+
let count = 0;
246+
const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise<Response> => {
247+
if (count++ === 0) {
248+
return new Response(undefined, {
249+
status: 429,
250+
headers: {
251+
'Retry-After': '0.1',
252+
},
253+
});
254+
}
255+
return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
256+
};
257+
258+
const client = new OpenAI({ apiKey: 'My API Key', fetch: testFetch });
259+
260+
expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
261+
expect(count).toEqual(2);
262+
expect(
263+
await client
264+
.request({ path: '/foo', method: 'get' })
265+
.asResponse()
266+
.then((r) => r.text()),
267+
).toEqual(JSON.stringify({ a: 1 }));
268+
expect(count).toEqual(3);
269+
});
270+
271+
test('retry on 429 with retry-after-ms', async () => {
272+
let count = 0;
273+
const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise<Response> => {
274+
if (count++ === 0) {
275+
return new Response(undefined, {
276+
status: 429,
277+
headers: {
278+
'Retry-After-Ms': '10',
279+
},
280+
});
281+
}
282+
return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
283+
};
284+
285+
const client = new OpenAI({ apiKey: 'My API Key', fetch: testFetch });
286+
287+
expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
288+
expect(count).toEqual(2);
289+
expect(
290+
await client
291+
.request({ path: '/foo', method: 'get' })
292+
.asResponse()
293+
.then((r) => r.text()),
294+
).toEqual(JSON.stringify({ a: 1 }));
295+
expect(count).toEqual(3);
296+
});
242297
});

0 commit comments

Comments
 (0)
Please sign in to comment.