@afoures/http-client 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +5 -1
- package/dist/lib/endpoint.d.mts +40 -0
- package/dist/lib/endpoint.mjs +191 -0
- package/dist/lib/errors.d.mts +54 -0
- package/dist/lib/errors.mjs +58 -0
- package/dist/lib/http-client.d.mts +64 -0
- package/dist/lib/http-client.mjs +134 -0
- package/dist/lib/types.d.mts +218 -0
- package/dist/lib/types.mjs +4 -0
- package/dist/lib/utils.mjs +69 -0
- package/docs/endpoint-definition.md +128 -0
- package/docs/error-handling.md +148 -0
- package/docs/http-client.md +147 -0
- package/docs/response-parsing.md +243 -0
- package/docs/retry-policy.md +197 -0
- package/docs/schema-integration.md +221 -0
- package/docs/serialization.md +211 -0
- package/package.json +7 -6
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# HTTP Client
|
|
2
|
+
|
|
3
|
+
The `http_client` function creates a typed API client from a map of endpoints.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Endpoint, http_client } from '@afoures/http-client'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
|
|
11
|
+
const api = http_client({
|
|
12
|
+
origin: 'https://api.example.com',
|
|
13
|
+
endpoints: {
|
|
14
|
+
users: new Endpoint({
|
|
15
|
+
method: 'GET',
|
|
16
|
+
pathname: '/users',
|
|
17
|
+
data: { schema: z.array(z.object({ id: z.string() })) },
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const result = await api.users({})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Organizing Endpoints
|
|
26
|
+
|
|
27
|
+
Nest endpoints in objects for logical grouping:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const api = http_client({
|
|
31
|
+
origin: 'https://api.example.com',
|
|
32
|
+
endpoints: {
|
|
33
|
+
users: {
|
|
34
|
+
list: new Endpoint({ method: 'GET', pathname: '/users' }),
|
|
35
|
+
get: new Endpoint({ method: 'GET', pathname: '/users/(:id)' }),
|
|
36
|
+
create: new Endpoint({ method: 'POST', pathname: '/users' }),
|
|
37
|
+
update: new Endpoint({ method: 'PUT', pathname: '/users/(:id)' }),
|
|
38
|
+
delete: new Endpoint({ method: 'DELETE', pathname: '/users/(:id)' }),
|
|
39
|
+
},
|
|
40
|
+
posts: {
|
|
41
|
+
list: new Endpoint({ method: 'GET', pathname: '/posts' }),
|
|
42
|
+
get: new Endpoint({ method: 'GET', pathname: '/posts/(:id)' }),
|
|
43
|
+
comments: {
|
|
44
|
+
list: new Endpoint({ method: 'GET', pathname: '/posts/(:postId)/comments' }),
|
|
45
|
+
create: new Endpoint({ method: 'POST', pathname: '/posts/(:postId)/comments' }),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Fully typed paths
|
|
52
|
+
await api.users.list({})
|
|
53
|
+
await api.users.get({ params: { id: '123' } })
|
|
54
|
+
await api.posts.comments.create({ params: { postId: '1' }, body: { text: 'Nice!' } })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Shared Options
|
|
58
|
+
|
|
59
|
+
Provide sync or async default options for all requests:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const api = http_client({
|
|
63
|
+
origin: 'https://api.example.com',
|
|
64
|
+
endpoints: { /* ... */ },
|
|
65
|
+
options: async () => {
|
|
66
|
+
const token = await getAuthToken()
|
|
67
|
+
return {
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${token}`,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Options are merged in this order (later overrides earlier):
|
|
77
|
+
1. `options()` from `http_client`
|
|
78
|
+
2. Endpoint default options
|
|
79
|
+
3. Per-request options
|
|
80
|
+
|
|
81
|
+
## Custom Fetch
|
|
82
|
+
|
|
83
|
+
Provide a custom fetch function for proxying, logging, or modifying requests:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const api = http_client({
|
|
87
|
+
origin: 'https://api.example.com',
|
|
88
|
+
endpoints: { /* ... */ },
|
|
89
|
+
fetch: async (request) => {
|
|
90
|
+
console.log('Request:', request.url)
|
|
91
|
+
const response = await fetch(request)
|
|
92
|
+
console.log('Response:', response.status)
|
|
93
|
+
return response
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For testing, use tools like [MSW](https://mswjs.io/) instead of custom fetch.
|
|
99
|
+
|
|
100
|
+
## Per-Request Options
|
|
101
|
+
|
|
102
|
+
All `RequestInit` options plus custom options can be passed per-request:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const result = await api.users.get({
|
|
106
|
+
params: { id: '123' },
|
|
107
|
+
headers: { 'X-Custom': 'value' },
|
|
108
|
+
signal: abortController.signal,
|
|
109
|
+
timeout: 5000,
|
|
110
|
+
retry: { attempts: 3, delay: 1000 },
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Headers with Reducers
|
|
115
|
+
|
|
116
|
+
Headers can be functions that receive the current value:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const endpoint = new Endpoint({
|
|
120
|
+
method: 'GET',
|
|
121
|
+
pathname: '/users',
|
|
122
|
+
headers: {
|
|
123
|
+
'X-Request-ID': (current) => current ?? crypto.randomUUID(),
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Response Handling
|
|
129
|
+
|
|
130
|
+
All endpoint functions return a union type:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
134
|
+
|
|
135
|
+
// Can be an error
|
|
136
|
+
if (result instanceof Error) {
|
|
137
|
+
// TimeoutError, NetworkError, SerializationError, etc.
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Or a response
|
|
142
|
+
if (result.ok) {
|
|
143
|
+
console.log(result.data)
|
|
144
|
+
} else {
|
|
145
|
+
console.log(result.error)
|
|
146
|
+
}
|
|
147
|
+
```
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Response Parsing
|
|
2
|
+
|
|
3
|
+
Endpoints parse HTTP responses into typed results based on status code.
|
|
4
|
+
|
|
5
|
+
## Response Types
|
|
6
|
+
|
|
7
|
+
### Successful Response (20x)
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
type SuccessfulResponse<Data> = {
|
|
11
|
+
ok: true
|
|
12
|
+
status: 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
|
|
13
|
+
data: Data
|
|
14
|
+
headers: Headers
|
|
15
|
+
raw_response: Response
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Redirect (30x)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
type RedirectMessage = {
|
|
23
|
+
ok: false
|
|
24
|
+
status: 300 | 301 | 302 | 303 | 304 | 307 | 308
|
|
25
|
+
redirect_to: string | null
|
|
26
|
+
headers: Headers
|
|
27
|
+
raw_response: Response
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Client Error (40x)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
type ClientErrorResponse<Error> = {
|
|
35
|
+
ok: false
|
|
36
|
+
status: 400 | 401 | 402 | 403 | 404 | /* ... */
|
|
37
|
+
error: Error
|
|
38
|
+
headers: Headers
|
|
39
|
+
raw_response: Response
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Server Error (50x)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
type ServerErrorResponse<Error> = {
|
|
47
|
+
ok: false
|
|
48
|
+
status: 500 | 501 | 502 | 503 | 504 | /* ... */
|
|
49
|
+
error: Error
|
|
50
|
+
headers: Headers
|
|
51
|
+
raw_response: Response
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Data Parser
|
|
56
|
+
|
|
57
|
+
Define a `data` parser for successful responses:
|
|
58
|
+
|
|
59
|
+
### JSON (Default)
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const endpoint = new Endpoint({
|
|
63
|
+
method: 'GET',
|
|
64
|
+
pathname: '/users/(:id)',
|
|
65
|
+
data: {
|
|
66
|
+
schema: z.object({
|
|
67
|
+
id: z.string(),
|
|
68
|
+
name: z.string(),
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const result = await endpoint.parse_response(response)
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
console.log(result.data) // { id: string, name: string }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Text
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const endpoint = new Endpoint({
|
|
83
|
+
method: 'GET',
|
|
84
|
+
pathname: '/health',
|
|
85
|
+
data: {
|
|
86
|
+
schema: z.string(),
|
|
87
|
+
deserialization: 'text',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Custom Deserialization
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const endpoint = new Endpoint({
|
|
96
|
+
method: 'GET',
|
|
97
|
+
pathname: '/data',
|
|
98
|
+
data: {
|
|
99
|
+
schema: z.object({ value: z.number() }),
|
|
100
|
+
deserialization: async (body) => {
|
|
101
|
+
const text = await new Response(body).text()
|
|
102
|
+
return JSON.parse(text)
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 204 No Content
|
|
109
|
+
|
|
110
|
+
For endpoints that return no content:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const endpoint = new Endpoint({
|
|
114
|
+
method: 'DELETE',
|
|
115
|
+
pathname: '/users/(:id)',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const result = await endpoint.parse_response(response)
|
|
119
|
+
// result.ok === true, result.status === 204, result.data === null
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Error Parser
|
|
123
|
+
|
|
124
|
+
Define an `error` parser for error responses:
|
|
125
|
+
|
|
126
|
+
### JSON
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const endpoint = new Endpoint({
|
|
130
|
+
method: 'POST',
|
|
131
|
+
pathname: '/users',
|
|
132
|
+
body: { schema: z.object({ name: z.string() }) },
|
|
133
|
+
error: {
|
|
134
|
+
schema: z.object({
|
|
135
|
+
message: z.string(),
|
|
136
|
+
code: z.string(),
|
|
137
|
+
}),
|
|
138
|
+
deserialization: 'json',
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const result = await endpoint.parse_response(response)
|
|
143
|
+
if (!result.ok && result.status === 400) {
|
|
144
|
+
console.log(result.error.message)
|
|
145
|
+
console.log(result.error.code)
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Text
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const endpoint = new Endpoint({
|
|
153
|
+
method: 'GET',
|
|
154
|
+
pathname: '/users/(:id)',
|
|
155
|
+
error: {
|
|
156
|
+
schema: z.string(),
|
|
157
|
+
deserialization: 'text',
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const result = await endpoint.parse_response(response)
|
|
162
|
+
if (!result.ok && result.status === 404) {
|
|
163
|
+
console.log(result.error) // "Not Found"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Default (No Schema)
|
|
168
|
+
|
|
169
|
+
Without an error parser, errors default to text:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const endpoint = new Endpoint({
|
|
173
|
+
method: 'GET',
|
|
174
|
+
pathname: '/users/(:id)',
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const result = await endpoint.parse_response(response)
|
|
178
|
+
if (!result.ok) {
|
|
179
|
+
console.log(typeof result.error) // "string"
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Schema Transforms
|
|
184
|
+
|
|
185
|
+
Schemas can transform response data:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const endpoint = new Endpoint({
|
|
189
|
+
method: 'GET',
|
|
190
|
+
pathname: '/users/(:id)',
|
|
191
|
+
data: {
|
|
192
|
+
schema: z.object({
|
|
193
|
+
name: z.string().transform(s => s.toUpperCase()),
|
|
194
|
+
createdAt: z.string().transform(s => new Date(s)),
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const result = await endpoint.parse_response(response)
|
|
200
|
+
if (result.ok) {
|
|
201
|
+
console.log(result.data.name) // uppercase string
|
|
202
|
+
console.log(result.data.createdAt) // Date object
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Deserialization Errors
|
|
207
|
+
|
|
208
|
+
If response parsing fails validation, a `DeserializationError` is returned:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const result = await endpoint.parse_response(response)
|
|
212
|
+
|
|
213
|
+
if (result instanceof DeserializationError) {
|
|
214
|
+
console.log(result.message) // "Response deserialization failed"
|
|
215
|
+
console.log(result.cause) // Schema validation issues
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Handling All Cases
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
223
|
+
|
|
224
|
+
if (result instanceof Error) {
|
|
225
|
+
// UnexpectedError, NetworkError, TimeoutError, etc.
|
|
226
|
+
console.log(result.message)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (result.ok) {
|
|
231
|
+
// 20x success
|
|
232
|
+
console.log(result.data)
|
|
233
|
+
} else if (result.status >= 300 && result.status < 400) {
|
|
234
|
+
// Redirect
|
|
235
|
+
console.log(result.redirect_to)
|
|
236
|
+
} else if (result.status >= 400 && result.status < 500) {
|
|
237
|
+
// Client error
|
|
238
|
+
console.log(result.error)
|
|
239
|
+
} else {
|
|
240
|
+
// Server error
|
|
241
|
+
console.log(result.error)
|
|
242
|
+
}
|
|
243
|
+
```
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Retry Policy
|
|
2
|
+
|
|
3
|
+
Configure automatic retries for failed requests.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
type RetryPolicy = {
|
|
9
|
+
attempts?: number | ((ctx: { request: Request }) => number | Promise<number>)
|
|
10
|
+
delay?: number | ((ctx: { request: Request; response?: Response; error?: Error; attempt: number }) => number | Promise<number>)
|
|
11
|
+
when?: (ctx: { request: Request; response?: Response; error?: Error }) => boolean | Promise<boolean>
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
### Fixed Attempts and Delay
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
const result = await api.users.get({
|
|
21
|
+
params: { id: '123' },
|
|
22
|
+
retry: {
|
|
23
|
+
attempts: 3,
|
|
24
|
+
delay: 1000, // 1 second
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Conditional Retry
|
|
30
|
+
|
|
31
|
+
By default, retries on non-OK responses. Customize with `when`:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const result = await api.users.get({
|
|
35
|
+
params: { id: '123' },
|
|
36
|
+
retry: {
|
|
37
|
+
attempts: 3,
|
|
38
|
+
delay: 1000,
|
|
39
|
+
when: ({ response, error }) => {
|
|
40
|
+
// Retry on server errors or network failures
|
|
41
|
+
if (error) return true
|
|
42
|
+
if (response && response.status >= 500) return true
|
|
43
|
+
return false
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Retry on Specific Status
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const result = await api.users.get({
|
|
53
|
+
params: { id: '123' },
|
|
54
|
+
retry: {
|
|
55
|
+
attempts: 5,
|
|
56
|
+
delay: 2000,
|
|
57
|
+
when: ({ response }) => {
|
|
58
|
+
if (!response) return false
|
|
59
|
+
return response.status === 503 // Service Unavailable
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Exponential Backoff
|
|
66
|
+
|
|
67
|
+
Use a delay function for exponential backoff:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const result = await api.users.get({
|
|
71
|
+
params: { id: '123' },
|
|
72
|
+
retry: {
|
|
73
|
+
attempts: 5,
|
|
74
|
+
delay: ({ attempt }) => Math.min(1000 * Math.pow(2, attempt), 30000),
|
|
75
|
+
when: ({ error }) => !!error,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Retry on All GET Requests
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const endpoint = new Endpoint({
|
|
84
|
+
method: 'GET',
|
|
85
|
+
pathname: '/users',
|
|
86
|
+
retry: {
|
|
87
|
+
attempts: 3,
|
|
88
|
+
delay: 1000,
|
|
89
|
+
when: ({ request }) => request.method === 'GET',
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Dynamic Attempts
|
|
95
|
+
|
|
96
|
+
Determine max attempts dynamically:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const result = await api.users.get({
|
|
100
|
+
params: { id: '123' },
|
|
101
|
+
retry: {
|
|
102
|
+
attempts: ({ request }) => {
|
|
103
|
+
// Check custom header or metadata
|
|
104
|
+
const priority = request.headers.get('X-Priority')
|
|
105
|
+
return priority === 'high' ? 5 : 2
|
|
106
|
+
},
|
|
107
|
+
delay: 1000,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Context Information
|
|
113
|
+
|
|
114
|
+
The `when` and `delay` functions receive context about the request:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
retry: {
|
|
118
|
+
when: ({ request, response, error }) => {
|
|
119
|
+
// request: The Request object
|
|
120
|
+
// response: The Response if received, undefined if network error
|
|
121
|
+
// error: NetworkError, TimeoutError, etc. if occurred
|
|
122
|
+
return true
|
|
123
|
+
},
|
|
124
|
+
delay: ({ request, response, error, attempt }) => {
|
|
125
|
+
// attempt: Current attempt number (1-indexed)
|
|
126
|
+
return attempt * 1000
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Default Behavior
|
|
132
|
+
|
|
133
|
+
Without a `when` condition, retries on non-OK responses:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
retry: {
|
|
137
|
+
attempts: 3,
|
|
138
|
+
delay: 0,
|
|
139
|
+
// when: defaults to ({ response }) => response?.ok === false
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Endpoint-Level Retry
|
|
144
|
+
|
|
145
|
+
Set default retry on the endpoint:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const endpoint = new Endpoint({
|
|
149
|
+
method: 'GET',
|
|
150
|
+
pathname: '/users',
|
|
151
|
+
retry: {
|
|
152
|
+
attempts: 3,
|
|
153
|
+
delay: 1000,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Per-request retry overrides endpoint defaults.
|
|
159
|
+
|
|
160
|
+
## AbortSignal with Retry
|
|
161
|
+
|
|
162
|
+
Retries respect `AbortSignal`:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const controller = new AbortController()
|
|
166
|
+
|
|
167
|
+
const result = await api.users.get({
|
|
168
|
+
params: { id: '123' },
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
retry: { attempts: 10, delay: 1000 },
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Call controller.abort() to cancel retries
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Example: Resilient API Client
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const api = http_client({
|
|
180
|
+
origin: 'https://api.example.com',
|
|
181
|
+
endpoints: {
|
|
182
|
+
users: new Endpoint({
|
|
183
|
+
method: 'GET',
|
|
184
|
+
pathname: '/users',
|
|
185
|
+
retry: {
|
|
186
|
+
attempts: 3,
|
|
187
|
+
delay: ({ attempt }) => Math.min(100 * Math.pow(2, attempt), 5000),
|
|
188
|
+
when: ({ response, error }) => {
|
|
189
|
+
if (error) return true
|
|
190
|
+
if (!response) return false
|
|
191
|
+
return response.status >= 500 || response.status === 429
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
```
|