豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 4bf637d

Browse files
authored
feat: add .type field to APIError for error kind identification (#790)
Expose `error.error.type` (e.g. `"rate_limit_error"`, `"overloaded_error"`) as a top-level `.type` field on `APIError`. This lets users identify error kinds uniformly across HTTP errors and streaming errors, since streaming errors arrive as SSE error events with HTTP 200 status (making `instanceof` and status code checks unreliable). Equivalent of stainless-sdks/anthropic-python#1587 for TypeScript. ```typescript try { for await (const event of stream) { /* ... */ } } catch (err) { if (err instanceof Anthropic.APIError) { if (err.type === 'rate_limit_error' || err.type === 'overloaded_error') { // retry after backoff — works for both streaming and non-streaming } } } ```
1 parent 4957a5e commit 4bf637d

File tree

4 files changed

+98
-11
lines changed

4 files changed

+98
-11
lines changed

src/core/error.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
import { castToError } from '../internal/errors';
4+
import type { ErrorType } from '../resources/shared';
45

56
export class AnthropicError extends Error {}
67

@@ -18,12 +19,22 @@ export class APIError<
1819

1920
readonly requestID: string | null | undefined;
2021

21-
constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) {
22+
/** The `error.type` from the API response body, e.g. `"rate_limit_error"` */
23+
readonly type: ErrorType | null;
24+
25+
constructor(
26+
status: TStatus,
27+
error: TError,
28+
message: string | undefined,
29+
headers: THeaders,
30+
type?: ErrorType | null,
31+
) {
2232
super(`${APIError.makeMessage(status, error, message)}`);
2333
this.status = status;
2434
this.headers = headers;
2535
this.requestID = headers?.get('request-id');
2636
this.error = error;
37+
this.type = type ?? null;
2738
}
2839

2940
private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
@@ -58,40 +69,41 @@ export class APIError<
5869
}
5970

6071
const error = errorResponse as Record<string, any>;
72+
const type = error?.['error']?.['type'] as ErrorType | undefined;
6173

6274
if (status === 400) {
63-
return new BadRequestError(status, error, message, headers);
75+
return new BadRequestError(status, error, message, headers, type);
6476
}
6577

6678
if (status === 401) {
67-
return new AuthenticationError(status, error, message, headers);
79+
return new AuthenticationError(status, error, message, headers, type);
6880
}
6981

7082
if (status === 403) {
71-
return new PermissionDeniedError(status, error, message, headers);
83+
return new PermissionDeniedError(status, error, message, headers, type);
7284
}
7385

7486
if (status === 404) {
75-
return new NotFoundError(status, error, message, headers);
87+
return new NotFoundError(status, error, message, headers, type);
7688
}
7789

7890
if (status === 409) {
79-
return new ConflictError(status, error, message, headers);
91+
return new ConflictError(status, error, message, headers, type);
8092
}
8193

8294
if (status === 422) {
83-
return new UnprocessableEntityError(status, error, message, headers);
95+
return new UnprocessableEntityError(status, error, message, headers, type);
8496
}
8597

8698
if (status === 429) {
87-
return new RateLimitError(status, error, message, headers);
99+
return new RateLimitError(status, error, message, headers, type);
88100
}
89101

90102
if (status >= 500) {
91-
return new InternalServerError(status, error, message, headers);
103+
return new InternalServerError(status, error, message, headers, type);
92104
}
93105

94-
return new APIError(status, error, message, headers);
106+
return new APIError(status, error, message, headers, type);
95107
}
96108
}
97109

src/core/streaming.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { loggerFor } from '../internal/utils/log';
1010
import type { BaseAnthropic } from '../client';
1111

1212
import { APIError } from './error';
13+
import type { ErrorType } from '../resources/shared';
1314

1415
type Bytes = string | ArrayBuffer | Uint8Array | null | undefined;
1516

@@ -80,7 +81,9 @@ export class Stream<Item> implements AsyncIterable<Item> {
8081
}
8182

8283
if (sse.event === 'error') {
83-
throw new APIError(undefined, safeJSON(sse.data) ?? sse.data, undefined, response.headers);
84+
const body = safeJSON(sse.data) ?? sse.data;
85+
const type = body?.error?.type as ErrorType | undefined;
86+
throw new APIError(undefined, body, undefined, response.headers, type);
8487
}
8588
}
8689
done = true;

tests/index.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
import { APIPromise } from '@anthropic-ai/sdk/core/api-promise';
4+
import { APIError } from '@anthropic-ai/sdk/core/error';
45

56
import util from 'node:util';
67
import Anthropic from '@anthropic-ai/sdk';
@@ -772,4 +773,35 @@ describe('retries', () => {
772773
).toEqual(JSON.stringify({ a: 1 }));
773774
expect(count).toEqual(3);
774775
});
776+
777+
test('HTTP error response exposes error type', async () => {
778+
const client = new Anthropic({
779+
apiKey: 'test-key',
780+
fetch: () =>
781+
Promise.resolve(
782+
new Response(
783+
JSON.stringify({
784+
type: 'error',
785+
error: { type: 'invalid_request_error', message: 'Bad request' },
786+
}),
787+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
788+
),
789+
),
790+
});
791+
792+
try {
793+
await client.messages.create({
794+
model: 'claude-sonnet-4-5-20250929',
795+
max_tokens: 1,
796+
messages: [{ role: 'user', content: 'hi' }],
797+
});
798+
throw new Error('Expected request to throw');
799+
} catch (err) {
800+
expect(err).toBeInstanceOf(APIError);
801+
if (err instanceof APIError) {
802+
expect(err.type).toBe('invalid_request_error');
803+
expect(err.status).toBe(400);
804+
}
805+
}
806+
});
775807
});

tests/streaming.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,43 @@ test('error handling', async () => {
243243
);
244244
await err.toBeInstanceOf(APIError);
245245
});
246+
247+
test('APIError.generate() exposes error type', () => {
248+
const error = APIError.generate(
249+
400,
250+
{ type: 'error', error: { type: 'invalid_request_error', message: 'Bad request' } },
251+
undefined,
252+
new Headers({ 'request-id': 'req_123' }),
253+
);
254+
expect(error.type).toBe('invalid_request_error');
255+
expect(error.status).toBe(400);
256+
});
257+
258+
test('APIError.generate() sets type to null when absent', () => {
259+
const error = APIError.generate(500, { message: 'Internal error' }, undefined, new Headers());
260+
expect(error.type).toBeNull();
261+
});
262+
263+
test('error event exposes error type', async () => {
264+
async function* body(): AsyncGenerator<Buffer> {
265+
yield Buffer.from('event: error\n');
266+
yield Buffer.from('data: {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}');
267+
yield Buffer.from('\n\n');
268+
}
269+
270+
const stream = Stream.fromSSEResponse(
271+
new Response(await ReadableStreamFrom(body())),
272+
new AbortController(),
273+
);
274+
275+
try {
276+
for await (const _event of stream) {
277+
}
278+
throw new Error('Expected stream to throw');
279+
} catch (err) {
280+
expect(err).toBeInstanceOf(APIError);
281+
if (err instanceof APIError) {
282+
expect(err.type).toBe('overloaded_error');
283+
}
284+
}
285+
});

0 commit comments

Comments
 (0)