Skip to content

Commit a2208a2

Browse files
authored
fix (provider/gateway): improve error message on client side timeouts (#12407)
Client side timeouts were confusing to the user, so we wanted to improve the error message to link to the docs and explain how to modify fetch and undci header timeouts. ## Manual Verification ``` pnpm tsx src/generate-text/gateway-timeout.ts ```
1 parent 847a9ac commit a2208a2

File tree

8 files changed

+480
-60
lines changed

8 files changed

+480
-60
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/gateway': patch
3+
---
4+
5+
fix (provider/gateway): added custom error class and message for client side timeouts

examples/ai-functions/package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"version": "0.0.0",
44
"private": true,
55
"dependencies": {
6-
"@ai-sdk/black-forest-labs": "workspace:*",
76
"@ai-sdk/alibaba": "workspace:*",
87
"@ai-sdk/amazon-bedrock": "workspace:*",
98
"@ai-sdk/anthropic": "workspace:*",
109
"@ai-sdk/assemblyai": "workspace:*",
1110
"@ai-sdk/azure": "workspace:*",
1211
"@ai-sdk/baseten": "workspace:*",
12+
"@ai-sdk/black-forest-labs": "workspace:*",
1313
"@ai-sdk/cerebras": "workspace:*",
1414
"@ai-sdk/cohere": "workspace:*",
1515
"@ai-sdk/deepgram": "workspace:*",
@@ -23,16 +23,17 @@
2323
"@ai-sdk/google": "workspace:*",
2424
"@ai-sdk/google-vertex": "workspace:*",
2525
"@ai-sdk/groq": "workspace:*",
26+
"@ai-sdk/huggingface": "workspace:*",
27+
"@ai-sdk/hume": "workspace:*",
2628
"@ai-sdk/klingai": "workspace:*",
2729
"@ai-sdk/lmnt": "workspace:*",
2830
"@ai-sdk/luma": "workspace:*",
29-
"@ai-sdk/hume": "workspace:*",
3031
"@ai-sdk/mcp": "workspace:*",
3132
"@ai-sdk/mistral": "workspace:*",
3233
"@ai-sdk/moonshotai": "workspace:*",
34+
"@ai-sdk/open-responses": "workspace:*",
3335
"@ai-sdk/openai": "workspace:*",
3436
"@ai-sdk/openai-compatible": "workspace:*",
35-
"@ai-sdk/open-responses": "workspace:*",
3637
"@ai-sdk/perplexity": "workspace:*",
3738
"@ai-sdk/prodia": "workspace:*",
3839
"@ai-sdk/provider": "workspace:*",
@@ -42,23 +43,23 @@
4243
"@ai-sdk/valibot": "workspace:*",
4344
"@ai-sdk/vercel": "workspace:*",
4445
"@ai-sdk/xai": "workspace:*",
45-
"@ai-sdk/huggingface": "workspace:*",
4646
"@google/generative-ai": "0.21.0",
47-
"google-auth-library": "^9.15.1",
4847
"@langfuse/otel": "^4.5.0",
4948
"@opentelemetry/auto-instrumentations-node": "0.54.0",
5049
"@opentelemetry/sdk-node": "^0.210.0",
5150
"@opentelemetry/sdk-trace-node": "^2.5.0",
51+
"@standard-schema/spec": "1.1.0",
5252
"@valibot/to-json-schema": "^1.3.0",
5353
"ai": "workspace:*",
5454
"arktype": "2.1.28",
5555
"dotenv": "16.4.5",
5656
"effect": "3.18.4",
57+
"google-auth-library": "^9.15.1",
5758
"image-type": "^5.2.0",
5859
"mathjs": "14.0.0",
5960
"sharp": "^0.33.5",
60-
"@standard-schema/spec": "1.1.0",
6161
"terminal-image": "^2.0.0",
62+
"undici": "^7.21.0",
6263
"valibot": "1.1.0",
6364
"zod": "3.25.76"
6465
},
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Example demonstrating Gateway timeout error handling
3+
*
4+
* This example uses undici with an extremely short timeout (1ms) to trigger
5+
* a timeout error. The Gateway SDK will catch this and provide a helpful
6+
* error message with troubleshooting guidance.
7+
*
8+
* Prerequisites:
9+
* - Set AI_GATEWAY_API_KEY environment variable
10+
* (See .env.example for setup instructions)
11+
*
12+
* Run: pnpm tsx src/generate-text/gateway-timeout.ts
13+
*/
14+
import { createGateway, generateText } from 'ai';
15+
import { Agent, fetch as undiciFetch } from 'undici';
16+
import { run } from '../lib/run';
17+
18+
run(async () => {
19+
try {
20+
// Create an undici Agent with very short timeouts
21+
// bodyTimeout applies to receiving the entire response body
22+
const agent = new Agent({
23+
headersTimeout: 1, // 1ms - will timeout waiting for headers
24+
bodyTimeout: 1, // 1ms - will timeout reading response body
25+
});
26+
27+
// Create custom fetch using undici with the configured agent
28+
const customFetch = (
29+
url: string | URL | Request,
30+
options?: RequestInit,
31+
): Promise<Response> => {
32+
return undiciFetch(url as Parameters<typeof undiciFetch>[0], {
33+
...(options as any),
34+
dispatcher: agent,
35+
}) as Promise<Response>;
36+
};
37+
38+
// Create gateway provider with custom fetch
39+
const gateway = createGateway({
40+
fetch: customFetch,
41+
});
42+
43+
console.log('Making request with 1ms timeout...');
44+
console.log(
45+
'This should timeout immediately and show the timeout error handling.\n',
46+
);
47+
48+
const { text, usage } = await generateText({
49+
model: gateway('anthropic/claude-3.5-sonnet'),
50+
prompt:
51+
'Write a detailed essay about the history of artificial intelligence, covering major milestones from the 1950s to present day.',
52+
});
53+
54+
console.log('Success! Response received:');
55+
console.log(text);
56+
console.log();
57+
console.log('Usage:', usage);
58+
} catch (error) {
59+
console.error(
60+
'╔════════════════════════════════════════════════════════════════╗',
61+
);
62+
console.error(
63+
'║ TIMEOUT ERROR CAUGHT ║',
64+
);
65+
console.error(
66+
'╚════════════════════════════════════════════════════════════════╝\n',
67+
);
68+
console.error('Error Name:', (error as Error).name);
69+
console.error('Error Type:', (error as any).type);
70+
console.error('Status Code:', (error as any).statusCode);
71+
console.error('Error Code:', (error as any).code);
72+
console.error('\nError Message:');
73+
console.error('─'.repeat(70));
74+
console.error((error as Error).message);
75+
console.error('─'.repeat(70));
76+
77+
// Log the cause to see the original undici error
78+
if ((error as any).cause) {
79+
console.error('\n📋 Original Error (cause):');
80+
console.error(' Name:', ((error as any).cause as Error).name);
81+
console.error(' Code:', ((error as any).cause as any).code);
82+
console.error(' Message:', ((error as any).cause as Error).message);
83+
console.error(
84+
' Constructor:',
85+
((error as any).cause as Error).constructor.name,
86+
);
87+
}
88+
}
89+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { APICallError } from '@ai-sdk/provider';
2+
import { describe, expect, it } from 'vitest';
3+
import { asGatewayError } from './as-gateway-error';
4+
import {
5+
GatewayError,
6+
GatewayTimeoutError,
7+
GatewayResponseError,
8+
} from './index';
9+
10+
describe('asGatewayError', () => {
11+
describe('timeout error detection', () => {
12+
it('should detect error with UND_ERR_HEADERS_TIMEOUT code', async () => {
13+
const error = Object.assign(new Error('Request timeout'), {
14+
code: 'UND_ERR_HEADERS_TIMEOUT',
15+
});
16+
17+
const result = await asGatewayError(error);
18+
19+
expect(GatewayTimeoutError.isInstance(result)).toBe(true);
20+
expect(result.message).toContain('Request timeout');
21+
});
22+
23+
it('should detect error with UND_ERR_BODY_TIMEOUT code', async () => {
24+
const error = Object.assign(new Error('Body timeout'), {
25+
code: 'UND_ERR_BODY_TIMEOUT',
26+
});
27+
28+
const result = await asGatewayError(error);
29+
30+
expect(GatewayTimeoutError.isInstance(result)).toBe(true);
31+
});
32+
33+
it('should detect error with UND_ERR_CONNECT_TIMEOUT code', async () => {
34+
const error = Object.assign(new Error('Connect timeout'), {
35+
code: 'UND_ERR_CONNECT_TIMEOUT',
36+
});
37+
38+
const result = await asGatewayError(error);
39+
40+
expect(GatewayTimeoutError.isInstance(result)).toBe(true);
41+
});
42+
});
43+
44+
describe('non-timeout errors', () => {
45+
it('should not treat network errors as timeout errors', async () => {
46+
const error = new Error('Network error');
47+
48+
const result = await asGatewayError(error);
49+
50+
expect(GatewayTimeoutError.isInstance(result)).toBe(false);
51+
expect(GatewayResponseError.isInstance(result)).toBe(true);
52+
expect(result.message).toContain('Gateway request failed: Network error');
53+
});
54+
55+
it('should not treat connection errors as timeout errors', async () => {
56+
const error = Object.assign(new Error('Connection refused'), {
57+
code: 'ECONNREFUSED',
58+
});
59+
60+
const result = await asGatewayError(error);
61+
62+
expect(GatewayTimeoutError.isInstance(result)).toBe(false);
63+
expect(GatewayResponseError.isInstance(result)).toBe(true);
64+
});
65+
66+
it('should pass through existing GatewayError instances', async () => {
67+
const existingError = GatewayTimeoutError.createTimeoutError({
68+
originalMessage: 'existing timeout',
69+
});
70+
71+
const result = await asGatewayError(existingError);
72+
73+
expect(result).toBe(existingError);
74+
});
75+
76+
it('should handle non-Error objects', async () => {
77+
const error = { message: 'timeout occurred' };
78+
79+
const result = await asGatewayError(error);
80+
81+
// Non-Error objects won't be detected as timeout errors
82+
expect(GatewayTimeoutError.isInstance(result)).toBe(false);
83+
expect(GatewayResponseError.isInstance(result)).toBe(true);
84+
});
85+
86+
it('should handle null', async () => {
87+
const result = await asGatewayError(null);
88+
89+
expect(GatewayTimeoutError.isInstance(result)).toBe(false);
90+
expect(GatewayResponseError.isInstance(result)).toBe(true);
91+
});
92+
93+
it('should handle undefined', async () => {
94+
const result = await asGatewayError(undefined);
95+
96+
expect(GatewayTimeoutError.isInstance(result)).toBe(false);
97+
expect(GatewayResponseError.isInstance(result)).toBe(true);
98+
});
99+
});
100+
101+
describe('error properties', () => {
102+
it('should preserve the original error as cause', async () => {
103+
const originalError = Object.assign(new Error('timeout error'), {
104+
code: 'UND_ERR_HEADERS_TIMEOUT',
105+
});
106+
107+
const result = await asGatewayError(originalError);
108+
109+
expect(result.cause).toBe(originalError);
110+
});
111+
112+
it('should set correct status code for timeout errors', async () => {
113+
const error = Object.assign(new Error('timeout'), {
114+
code: 'UND_ERR_HEADERS_TIMEOUT',
115+
});
116+
117+
const result = await asGatewayError(error);
118+
119+
expect(result.statusCode).toBe(408);
120+
});
121+
122+
it('should have correct error type', async () => {
123+
const error = Object.assign(new Error('timeout'), {
124+
code: 'UND_ERR_HEADERS_TIMEOUT',
125+
});
126+
127+
const result = await asGatewayError(error);
128+
129+
expect(result.type).toBe('timeout_error');
130+
});
131+
});
132+
133+
describe('APICallError with timeout cause', () => {
134+
it('should detect timeout when APICallError has UND_ERR_HEADERS_TIMEOUT in cause', async () => {
135+
const timeoutError = Object.assign(new Error('Request timeout'), {
136+
code: 'UND_ERR_HEADERS_TIMEOUT',
137+
});
138+
139+
const apiCallError = new APICallError({
140+
message: 'Cannot connect to API: Request timeout',
141+
url: 'https://example.com',
142+
requestBodyValues: {},
143+
cause: timeoutError,
144+
});
145+
146+
const result = await asGatewayError(apiCallError);
147+
148+
expect(GatewayTimeoutError.isInstance(result)).toBe(true);
149+
expect(result.message).toContain('Gateway request timed out');
150+
});
151+
152+
it('should not treat APICallError as timeout if cause is not timeout-related', async () => {
153+
const networkError = new Error('Network connection failed');
154+
155+
const apiCallError = new APICallError({
156+
message: 'Cannot connect to API: Network connection failed',
157+
url: 'https://example.com',
158+
requestBodyValues: {},
159+
cause: networkError,
160+
statusCode: 500,
161+
responseBody: JSON.stringify({
162+
error: { message: 'Internal error', type: 'internal_error' },
163+
}),
164+
});
165+
166+
const result = await asGatewayError(apiCallError);
167+
168+
expect(GatewayTimeoutError.isInstance(result)).toBe(false);
169+
});
170+
});
171+
});

packages/gateway/src/errors/as-gateway-error.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,58 @@
11
import { APICallError } from '@ai-sdk/provider';
22
import { extractApiCallResponse, GatewayError } from '.';
33
import { createGatewayErrorFromResponse } from './create-gateway-error';
4+
import { GatewayTimeoutError } from './gateway-timeout-error';
45

5-
export function asGatewayError(
6+
/**
7+
* Checks if an error is a timeout error from undici.
8+
* Only checks undici-specific error codes to avoid false positives.
9+
*/
10+
function isTimeoutError(error: unknown): boolean {
11+
if (!(error instanceof Error)) {
12+
return false;
13+
}
14+
15+
// Check for undici-specific timeout error codes
16+
const errorCode = (error as any).code;
17+
if (typeof errorCode === 'string') {
18+
const undiciTimeoutCodes = [
19+
'UND_ERR_HEADERS_TIMEOUT',
20+
'UND_ERR_BODY_TIMEOUT',
21+
'UND_ERR_CONNECT_TIMEOUT',
22+
];
23+
return undiciTimeoutCodes.includes(errorCode);
24+
}
25+
26+
return false;
27+
}
28+
29+
export async function asGatewayError(
630
error: unknown,
731
authMethod?: 'api-key' | 'oidc',
832
) {
933
if (GatewayError.isInstance(error)) {
1034
return error;
1135
}
1236

37+
// Check if this is a timeout error (or has a timeout error in the cause chain)
38+
if (isTimeoutError(error)) {
39+
return GatewayTimeoutError.createTimeoutError({
40+
originalMessage: error instanceof Error ? error.message : 'Unknown error',
41+
cause: error,
42+
});
43+
}
44+
45+
// Check if this is an APICallError caused by a timeout
1346
if (APICallError.isInstance(error)) {
14-
return createGatewayErrorFromResponse({
47+
// Check if the cause is a timeout error
48+
if (error.cause && isTimeoutError(error.cause)) {
49+
return GatewayTimeoutError.createTimeoutError({
50+
originalMessage: error.message,
51+
cause: error,
52+
});
53+
}
54+
55+
return await createGatewayErrorFromResponse({
1556
response: extractApiCallResponse(error),
1657
statusCode: error.statusCode ?? 500,
1758
defaultMessage: 'Gateway request failed',
@@ -20,7 +61,7 @@ export function asGatewayError(
2061
});
2162
}
2263

23-
return createGatewayErrorFromResponse({
64+
return await createGatewayErrorFromResponse({
2465
response: {},
2566
statusCode: 500,
2667
defaultMessage:

0 commit comments

Comments
 (0)