Skip to content

Commit 00ec8ac

Browse files
committed
fix: narrow 404 usage-limit remapping
The 404 error remapper was converting ambiguous responses into 429s based on free-form body text and generic rate_limit_exceeded codes. That makes not-found errors look like cooldown signals and feeds weak rate-limit reasons downstream. Only remap structured 404 responses that carry explicit usage_limit codes, while preserving entitlement detection. This keeps quota-limit signals intact without manufacturing ambiguous 429s from plain text or generic rate-limit payloads.
1 parent c6d0b5e commit 00ec8ac

2 files changed

Lines changed: 54 additions & 19 deletions

File tree

lib/request/fetch-helpers.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -978,8 +978,8 @@ async function safeReadBody(response: Response): Promise<string> {
978978
}
979979

980980
function mapUsageLimit404WithBody(response: Response, bodyText: string): Response | null {
981-
if (response.status !== HTTP_STATUS.NOT_FOUND) return null;
982-
if (!bodyText) return null;
981+
if (response.status !== HTTP_STATUS.NOT_FOUND) return null;
982+
if (!bodyText) return null;
983983

984984
let code = "";
985985
try {
@@ -994,8 +994,11 @@ function mapUsageLimit404WithBody(response: Response, bodyText: string): Respons
994994
return createEntitlementErrorResponse(bodyText);
995995
}
996996

997-
const haystack = `${code} ${bodyText}`.toLowerCase();
998-
if (!/usage_limit_reached|rate_limit_exceeded|usage limit/i.test(haystack)) {
997+
// Only structured quota-limit codes should be remapped from 404 to 429.
998+
// Free-text 404 bodies and generic rate_limit_* codes are too ambiguous and
999+
// degrade downstream rate-limit reason classification to "unknown".
1000+
const normalizedCode = code.toLowerCase();
1001+
if (!normalizedCode.includes("usage_limit")) {
9991002
return null;
10001003
}
10011004

test/fetch-helpers.test.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -464,25 +464,40 @@ describe('Fetch Helpers Module', () => {
464464
expect(headers.get('accept')).toBe('text/event-stream');
465465
});
466466

467-
it('maps usage-limit 404 errors to 429', async () => {
468-
const body = {
469-
error: {
470-
code: 'usage_limit_reached',
471-
message: 'limit reached',
467+
it('maps usage-limit 404 errors to 429', async () => {
468+
const body = {
469+
error: {
470+
code: 'usage_limit_reached',
471+
message: 'limit reached',
472472
},
473473
};
474474
const resp = new Response(JSON.stringify(body), { status: 404 });
475475
const { response: mapped, rateLimit } = await handleErrorResponse(resp);
476476
expect(mapped.status).toBe(429);
477477
const json = await mapped.json() as any;
478-
expect(json.error.code).toBe('usage_limit_reached');
479-
expect(rateLimit?.retryAfterMs).toBeGreaterThan(0);
480-
});
478+
expect(json.error.code).toBe('usage_limit_reached');
479+
expect(rateLimit?.retryAfterMs).toBeGreaterThan(0);
480+
});
481481

482-
it('leaves non-usage 404 errors unchanged', async () => {
483-
const body = { error: { code: 'not_found', message: 'nope' } };
484-
const resp = new Response(JSON.stringify(body), { status: 404 });
485-
const { response: result, rateLimit } = await handleErrorResponse(resp);
482+
it('maps usage-limit 404 errors to 429 when the signal comes from error.type', async () => {
483+
const body = {
484+
error: {
485+
type: 'usage_limit_reached',
486+
message: 'limit reached',
487+
},
488+
};
489+
const resp = new Response(JSON.stringify(body), { status: 404 });
490+
const { response: mapped, rateLimit } = await handleErrorResponse(resp);
491+
expect(mapped.status).toBe(429);
492+
const json = await mapped.json() as { error: { type?: string } };
493+
expect(json.error.type).toBe('usage_limit_reached');
494+
expect(rateLimit?.retryAfterMs).toBeGreaterThan(0);
495+
});
496+
497+
it('leaves non-usage 404 errors unchanged', async () => {
498+
const body = { error: { code: 'not_found', message: 'nope' } };
499+
const resp = new Response(JSON.stringify(body), { status: 404 });
500+
const { response: result, rateLimit } = await handleErrorResponse(resp);
486501
expect(result.status).toBe(404);
487502
const json = await result.json() as any;
488503
expect(json.error.code).toBe('not_found');
@@ -981,13 +996,30 @@ describe('createEntitlementErrorResponse', () => {
981996
});
982997

983998
describe('handleErrorResponse edge cases', () => {
984-
it('handles 404 with non-JSON body containing usage limit text', async () => {
999+
it('does not remap 404s with free-text usage-limit messages', async () => {
9851000
const response = new Response('usage limit exceeded - please try again', { status: 404 });
9861001

9871002
const { response: result, rateLimit } = await handleErrorResponse(response);
9881003

989-
expect(result.status).toBe(429);
990-
expect(rateLimit?.retryAfterMs).toBeGreaterThan(0);
1004+
expect(result.status).toBe(404);
1005+
expect(rateLimit).toBeUndefined();
1006+
});
1007+
1008+
it('does not remap 404s with generic rate_limit_exceeded codes', async () => {
1009+
const response = new Response(
1010+
JSON.stringify({
1011+
error: {
1012+
code: 'rate_limit_exceeded',
1013+
message: 'upstream overloaded',
1014+
},
1015+
}),
1016+
{ status: 404 },
1017+
);
1018+
1019+
const { response: result, rateLimit } = await handleErrorResponse(response);
1020+
1021+
expect(result.status).toBe(404);
1022+
expect(rateLimit).toBeUndefined();
9911023
});
9921024

9931025
it('does not treat non-429 rate-limit text as a cooldown signal', async () => {

0 commit comments

Comments
 (0)