@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,218 @@
|
|
|
1
|
+
import { AbortedError, NetworkError, TimeoutError, UnexpectedError } from "./errors.mjs";
|
|
2
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
+
import { Params } from "@remix-run/route-pattern";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/types.d.ts
|
|
6
|
+
type Pretty<T> = { [K in keyof T]: T[K] } & {};
|
|
7
|
+
type is_any<T> = boolean extends (T extends never ? true : false) ? true : false;
|
|
8
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
9
|
+
declare const ZeroWidthSpace = "\u200B";
|
|
10
|
+
/** Unrendered character (U+200B) used to mark a string type */
|
|
11
|
+
type ZeroWidthSpace = typeof ZeroWidthSpace;
|
|
12
|
+
type ErrorMessage<message extends string = string> = `error: ${message}${ZeroWidthSpace}`;
|
|
13
|
+
declare namespace Pathname {
|
|
14
|
+
type Relative = `/${string}`;
|
|
15
|
+
type WithParams = `${string}:${string}`;
|
|
16
|
+
type Params<pathname extends Pathname.Relative> = Pretty<{ [param in keyof Params<pathname>]: Params<pathname>[param] | number }>;
|
|
17
|
+
type DefaultParamsObjectSchema<pathname extends Pathname.Relative> = pathname extends Pathname.WithParams ? Schema._<Pathname.Params<pathname>> : never;
|
|
18
|
+
}
|
|
19
|
+
declare namespace HTTPStatus {
|
|
20
|
+
type InformationalResponse = 100 | 101 | 102 | 103;
|
|
21
|
+
type SuccessfulResponse = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226;
|
|
22
|
+
type RedirectMessage = 300 | 301 | 302 | 303 | 304 | 307 | 308;
|
|
23
|
+
type ClientErrorResponse = 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451;
|
|
24
|
+
type ServerErrorResponse = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511;
|
|
25
|
+
}
|
|
26
|
+
declare namespace HTTPMethod {
|
|
27
|
+
type WithBody = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
28
|
+
type WithoutBody = "GET";
|
|
29
|
+
type Any = HTTPMethod.WithoutBody | HTTPMethod.WithBody;
|
|
30
|
+
}
|
|
31
|
+
type HeaderValue = string | number | boolean | null | undefined;
|
|
32
|
+
type HeaderReducer = (current_value: string | undefined) => string | undefined | null;
|
|
33
|
+
type HeadersInitWithReducer = [string, HeaderValue | HeaderReducer][] | Record<string, HeaderValue | HeaderReducer> | Headers;
|
|
34
|
+
declare namespace RetryPolicy {
|
|
35
|
+
type Condition = (context: {
|
|
36
|
+
request: Request;
|
|
37
|
+
response: Response | undefined;
|
|
38
|
+
error: UnexpectedError | NetworkError | TimeoutError | AbortedError | undefined;
|
|
39
|
+
}) => MaybePromise<boolean>;
|
|
40
|
+
type Attempts = number | ((context: {
|
|
41
|
+
request: Request;
|
|
42
|
+
}) => MaybePromise<number>);
|
|
43
|
+
type Delay = number | ((context: {
|
|
44
|
+
response: Response | undefined;
|
|
45
|
+
error: UnexpectedError | NetworkError | TimeoutError | AbortedError | undefined;
|
|
46
|
+
request: Request;
|
|
47
|
+
attempt: number;
|
|
48
|
+
}) => MaybePromise<number>);
|
|
49
|
+
type Configuration = {
|
|
50
|
+
/**
|
|
51
|
+
* the number of attempts to make before giving up
|
|
52
|
+
*/
|
|
53
|
+
attempts?: Attempts;
|
|
54
|
+
/**
|
|
55
|
+
* the delay before retrying
|
|
56
|
+
*/
|
|
57
|
+
delay?: Delay;
|
|
58
|
+
/**
|
|
59
|
+
* function to determine if a retry attempt should be made
|
|
60
|
+
*/
|
|
61
|
+
when?: Condition;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
declare namespace HTTPFetch {
|
|
65
|
+
type SharedResponseContent = {
|
|
66
|
+
headers: Headers;
|
|
67
|
+
raw_response: Response;
|
|
68
|
+
};
|
|
69
|
+
export type ClientErrorResponse<Error> = SharedResponseContent & {
|
|
70
|
+
ok: false;
|
|
71
|
+
status: HTTPStatus.ClientErrorResponse;
|
|
72
|
+
error: Error;
|
|
73
|
+
};
|
|
74
|
+
export type ServerErrorResponse<Error> = SharedResponseContent & {
|
|
75
|
+
ok: false;
|
|
76
|
+
status: HTTPStatus.ServerErrorResponse;
|
|
77
|
+
error: Error;
|
|
78
|
+
};
|
|
79
|
+
export type SuccessfulResponse<Data> = SharedResponseContent & {
|
|
80
|
+
ok: true;
|
|
81
|
+
} & ({
|
|
82
|
+
status: Exclude<HTTPStatus.SuccessfulResponse, 204>;
|
|
83
|
+
data: Data;
|
|
84
|
+
} | {
|
|
85
|
+
status: 204;
|
|
86
|
+
data: null;
|
|
87
|
+
});
|
|
88
|
+
export type RedirectMessage = SharedResponseContent & {
|
|
89
|
+
ok: false;
|
|
90
|
+
status: HTTPStatus.RedirectMessage;
|
|
91
|
+
redirect_to: string | null;
|
|
92
|
+
};
|
|
93
|
+
export type TypedParamsInit<pathname extends Pathname.Relative, params_schema extends Schema._> = is_any<params_schema> extends true ? {
|
|
94
|
+
params: any;
|
|
95
|
+
} : [params_schema] extends [never] ? pathname extends Pathname.WithParams ? {
|
|
96
|
+
params: Pathname.Params<pathname>;
|
|
97
|
+
} : {} : {
|
|
98
|
+
params: Schema.infer_input<params_schema>;
|
|
99
|
+
};
|
|
100
|
+
export type TypedQueryInit<query_schema extends Schema._> = is_any<query_schema> extends true ? {
|
|
101
|
+
query: any;
|
|
102
|
+
} : [query_schema] extends [never] ? {} : undefined extends Schema.infer_input<query_schema> ? {
|
|
103
|
+
query?: Schema.infer_input<query_schema>;
|
|
104
|
+
} : {
|
|
105
|
+
query: Schema.infer_input<query_schema>;
|
|
106
|
+
};
|
|
107
|
+
export type TypedBodyInit<body_schema extends Schema._> = is_any<body_schema> extends true ? {
|
|
108
|
+
body: any;
|
|
109
|
+
} : [body_schema] extends [never] ? {} : undefined extends Schema.infer_input<body_schema> ? {
|
|
110
|
+
body?: Schema.infer_input<body_schema>;
|
|
111
|
+
} : {
|
|
112
|
+
body: Schema.infer_input<body_schema>;
|
|
113
|
+
};
|
|
114
|
+
export type DefaultRequestInit = {
|
|
115
|
+
headers?: HeadersInitWithReducer;
|
|
116
|
+
} & Omit<RequestInit, "body" | "method" | "headers">;
|
|
117
|
+
export type OptionalRequestInit = {
|
|
118
|
+
/**
|
|
119
|
+
* timeout in milliseconds
|
|
120
|
+
*/
|
|
121
|
+
timeout?: number;
|
|
122
|
+
/**
|
|
123
|
+
* retry policy
|
|
124
|
+
*/
|
|
125
|
+
retry?: RetryPolicy.Configuration;
|
|
126
|
+
};
|
|
127
|
+
export {};
|
|
128
|
+
}
|
|
129
|
+
declare namespace Schema {
|
|
130
|
+
type _<input = unknown, output = input> = StandardSchemaV1<input, output>;
|
|
131
|
+
type Any = Schema._<any, any>;
|
|
132
|
+
type Unknown = Schema._<unknown, unknown>;
|
|
133
|
+
type infer_input<schema extends Schema.Any, default_value extends unknown = never> = [default_value] extends [never] ? StandardSchemaV1.InferInput<schema> : [schema] extends [never] ? default_value : StandardSchemaV1.InferInput<schema>;
|
|
134
|
+
type infer_output<schema extends Schema.Any, default_value extends unknown = never> = [default_value] extends [never] ? StandardSchemaV1.InferOutput<schema> : [schema] extends [never] ? default_value : StandardSchemaV1.InferOutput<schema>;
|
|
135
|
+
}
|
|
136
|
+
declare namespace Json {
|
|
137
|
+
/**
|
|
138
|
+
Matches a JSON object.
|
|
139
|
+
@category JSON
|
|
140
|
+
*/
|
|
141
|
+
type Object = { [Key in string]: Json.Value };
|
|
142
|
+
/**
|
|
143
|
+
Matches a JSON array.
|
|
144
|
+
@category JSON
|
|
145
|
+
*/
|
|
146
|
+
type Array = Json.Value[] | readonly Json.Value[];
|
|
147
|
+
/**
|
|
148
|
+
Matches any valid JSON primitive value.
|
|
149
|
+
@category JSON
|
|
150
|
+
*/
|
|
151
|
+
type Primitive = string | number | boolean | null;
|
|
152
|
+
/**
|
|
153
|
+
Matches any valid JSON value.
|
|
154
|
+
@category JSON
|
|
155
|
+
*/
|
|
156
|
+
type Value = Json.Primitive | Json.Object | Json.Array;
|
|
157
|
+
}
|
|
158
|
+
declare namespace Serializer {
|
|
159
|
+
type Any = {
|
|
160
|
+
schema: Schema.Any;
|
|
161
|
+
serialization?: string | ((data: any) => any);
|
|
162
|
+
};
|
|
163
|
+
type Params<pathname extends Pathname.Relative, schema extends Schema._> = schema extends Schema._<any, Pathname.Params<pathname>> ? {
|
|
164
|
+
schema: schema;
|
|
165
|
+
serialization?: (data: Schema.infer_output<NoInfer<schema>>) => Pathname.Params<pathname>;
|
|
166
|
+
} : {
|
|
167
|
+
schema: schema;
|
|
168
|
+
serialization: (data: Schema.infer_output<NoInfer<schema>>) => Pathname.Params<pathname>;
|
|
169
|
+
};
|
|
170
|
+
type QueryString<schema extends Schema._> = schema extends Schema._<any, Array<Array<string>> | Record<string, string> | undefined> ? {
|
|
171
|
+
schema: schema;
|
|
172
|
+
serialization?: "urlencoded" | ((data: Schema.infer_output<NoInfer<schema>>) => URLSearchParams);
|
|
173
|
+
} : {
|
|
174
|
+
schema: schema;
|
|
175
|
+
serialization: (data: Schema.infer_output<NoInfer<schema>>) => URLSearchParams;
|
|
176
|
+
};
|
|
177
|
+
type Body<schema extends Schema._> = schema extends Schema._<any, Json.Value> ? {
|
|
178
|
+
schema: schema;
|
|
179
|
+
serialization?: "json" | ((data: Schema.infer_output<NoInfer<schema>>) => {
|
|
180
|
+
body: BodyInit | null;
|
|
181
|
+
content_type: string;
|
|
182
|
+
});
|
|
183
|
+
} : {
|
|
184
|
+
schema: schema;
|
|
185
|
+
serialization: (data: Schema.infer_output<NoInfer<schema>>) => {
|
|
186
|
+
body: BodyInit | null;
|
|
187
|
+
content_type: string;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
declare namespace Parser {
|
|
192
|
+
type Any = {
|
|
193
|
+
schema: Schema.Any;
|
|
194
|
+
deserialization?: string | ((data: any) => any);
|
|
195
|
+
};
|
|
196
|
+
type Data<schema extends Schema._> = schema extends Schema._<string, any> ? {
|
|
197
|
+
schema: schema;
|
|
198
|
+
deserialization: "text" | "json" | ((body: Response["body"]) => Promise<Schema.infer_input<NoInfer<schema>>>);
|
|
199
|
+
} : schema extends Schema._<Json.Value, any> ? {
|
|
200
|
+
schema: schema;
|
|
201
|
+
deserialization?: "json" | ((body: Response["body"]) => Promise<Schema.infer_input<NoInfer<schema>>>);
|
|
202
|
+
} : {
|
|
203
|
+
schema: schema;
|
|
204
|
+
deserialization: (body: Response["body"]) => Promise<Schema.infer_input<NoInfer<schema>>>;
|
|
205
|
+
};
|
|
206
|
+
type Error<schema extends Schema._> = schema extends Schema._<string, any> ? {
|
|
207
|
+
schema: schema;
|
|
208
|
+
deserialization: "text" | "json" | ((body: Response["body"]) => Promise<Schema.infer_input<NoInfer<schema>>>);
|
|
209
|
+
} : schema extends Schema._<Json.Value, any> ? {
|
|
210
|
+
schema: schema;
|
|
211
|
+
deserialization: "json" | ((body: Response["body"]) => Promise<Schema.infer_input<NoInfer<schema>>>);
|
|
212
|
+
} : {
|
|
213
|
+
schema: schema;
|
|
214
|
+
deserialization: (body: Response["body"]) => Promise<Schema.infer_input<NoInfer<schema>>>;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { ErrorMessage, HTTPFetch, HTTPMethod, HTTPStatus, HeadersInitWithReducer, MaybePromise, Parser, Pathname, Pretty, Schema, Serializer };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/lib/utils.ts
|
|
2
|
+
function get_entries(source) {
|
|
3
|
+
if (source instanceof Headers) return source.entries();
|
|
4
|
+
if (Array.isArray(source)) return source;
|
|
5
|
+
return Object.entries(source);
|
|
6
|
+
}
|
|
7
|
+
function merge_headers(...sources) {
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
for (const source of sources) {
|
|
10
|
+
if (!source) continue;
|
|
11
|
+
for (const [raw_key, value_or_reducer] of get_entries(source)) {
|
|
12
|
+
const key = raw_key.toLowerCase();
|
|
13
|
+
if (typeof value_or_reducer === "function") {
|
|
14
|
+
const new_value = value_or_reducer(headers.get(key) ?? void 0);
|
|
15
|
+
if (new_value != null) headers.set(key, new_value);
|
|
16
|
+
else headers.delete(key);
|
|
17
|
+
} else if (value_or_reducer == null) headers.delete(key);
|
|
18
|
+
else headers.set(key, value_or_reducer.toString());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return headers;
|
|
22
|
+
}
|
|
23
|
+
function extract_args(input) {
|
|
24
|
+
const { params, query, body, ...rest } = input;
|
|
25
|
+
return {
|
|
26
|
+
options: rest,
|
|
27
|
+
args: {
|
|
28
|
+
params,
|
|
29
|
+
query,
|
|
30
|
+
body
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function remove_custom_options(options) {
|
|
35
|
+
const { timeout: _timeout, headers: _headers, signal: _signal, retry: _retry, ...rest } = options;
|
|
36
|
+
return rest;
|
|
37
|
+
}
|
|
38
|
+
function merge_options(...sources) {
|
|
39
|
+
return {
|
|
40
|
+
...sources.reduce((acc, source) => {
|
|
41
|
+
return {
|
|
42
|
+
...acc,
|
|
43
|
+
...source,
|
|
44
|
+
signal: acc.signal ? source.signal ? AbortSignal.any([acc.signal, source.signal]) : acc.signal : source.signal,
|
|
45
|
+
retry: {
|
|
46
|
+
...acc.retry,
|
|
47
|
+
...source.retry
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}, {}),
|
|
51
|
+
headers: merge_headers(...sources.map((source) => source.headers))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms, signal) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
signal?.addEventListener("abort", on_abort, { once: true });
|
|
57
|
+
const token = setTimeout(() => {
|
|
58
|
+
signal?.removeEventListener("abort", on_abort);
|
|
59
|
+
resolve();
|
|
60
|
+
}, ms);
|
|
61
|
+
function on_abort() {
|
|
62
|
+
clearTimeout(token);
|
|
63
|
+
reject(signal.reason);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
export { extract_args, merge_options, remove_custom_options, sleep };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Endpoint Definition
|
|
2
|
+
|
|
3
|
+
The `Endpoint` class defines an HTTP endpoint with its method, path, serializers, and parsers.
|
|
4
|
+
|
|
5
|
+
## Constructor
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const endpoint = new Endpoint({
|
|
9
|
+
method: 'GET',
|
|
10
|
+
pathname: '/users/(:id)',
|
|
11
|
+
// ...options
|
|
12
|
+
})
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Options
|
|
16
|
+
|
|
17
|
+
### `method` (required)
|
|
18
|
+
|
|
19
|
+
HTTP method for the endpoint:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `GET` - Cannot have a body schema
|
|
26
|
+
- `POST`, `PUT`, `PATCH`, `DELETE` - Can have a body schema
|
|
27
|
+
|
|
28
|
+
### `pathname` (required)
|
|
29
|
+
|
|
30
|
+
URL path with optional dynamic segments:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
pathname: '/users' // Static path
|
|
34
|
+
pathname: '/users/:id' // Required param
|
|
35
|
+
pathname: '/users/(:id)' // Optional param
|
|
36
|
+
pathname: '/posts/:id/comments/:commentId' // Multiple params
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `params`
|
|
40
|
+
|
|
41
|
+
Serializer for path parameters. See [Serialization](./serialization.md#params).
|
|
42
|
+
|
|
43
|
+
### `query`
|
|
44
|
+
|
|
45
|
+
Serializer for query string parameters. See [Serialization](./serialization.md#query).
|
|
46
|
+
|
|
47
|
+
### `body`
|
|
48
|
+
|
|
49
|
+
Serializer for request body. See [Serialization](./serialization.md#body).
|
|
50
|
+
|
|
51
|
+
### `data`
|
|
52
|
+
|
|
53
|
+
Parser for successful response body. See [Response Parsing](./response-parsing.md#data).
|
|
54
|
+
|
|
55
|
+
### `error`
|
|
56
|
+
|
|
57
|
+
Parser for error response body. See [Response Parsing](./response-parsing.md#error).
|
|
58
|
+
|
|
59
|
+
## Constructor
|
|
60
|
+
|
|
61
|
+
The `Endpoint` constructor takes a definition and optional default options:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const endpoint = new Endpoint({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
pathname: '/users',
|
|
67
|
+
// Definition: method, pathname, params, query, body, data, error
|
|
68
|
+
}, {
|
|
69
|
+
headers: {
|
|
70
|
+
'X-API-Version': '2',
|
|
71
|
+
},
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
retry: {
|
|
74
|
+
attempts: 3,
|
|
75
|
+
delay: 1000,
|
|
76
|
+
when: ({ response }) => response?.status === 503,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The second argument accepts:
|
|
82
|
+
- `headers`: Default headers for all requests
|
|
83
|
+
- `timeout`: Request timeout in milliseconds
|
|
84
|
+
- `retry`: Default retry policy
|
|
85
|
+
|
|
86
|
+
These can be overridden per-request.
|
|
87
|
+
|
|
88
|
+
See [Retry Policy](./retry-policy.md) for retry configuration.
|
|
89
|
+
|
|
90
|
+
## Low-level Methods
|
|
91
|
+
|
|
92
|
+
Most users should use `http_client` instead of calling these methods directly. The HTTP client handles URL generation, body serialization, and response parsing automatically.
|
|
93
|
+
|
|
94
|
+
### `generate_url(init)`
|
|
95
|
+
|
|
96
|
+
Generates a full URL with params and query serialized:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const url = await endpoint.generate_url({
|
|
100
|
+
origin: 'https://api.example.com',
|
|
101
|
+
params: { id: '123' },
|
|
102
|
+
query: { include: 'posts' },
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Returns `URL` on success or `SerializationError` on validation failure.
|
|
107
|
+
|
|
108
|
+
### `serialize_body(init)`
|
|
109
|
+
|
|
110
|
+
Serializes the request body:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const { body, content_type } = await endpoint.serialize_body({
|
|
114
|
+
body: { name: 'John' },
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Returns `{ body, content_type }` on success or `SerializationError` on validation failure.
|
|
119
|
+
|
|
120
|
+
### `parse_response(response)`
|
|
121
|
+
|
|
122
|
+
Parses an HTTP response:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const result = await endpoint.parse_response(response)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Returns typed result based on status code. See [Response Parsing](./response-parsing.md).
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
The HTTP client provides typed errors for different failure scenarios.
|
|
4
|
+
|
|
5
|
+
## Error Types
|
|
6
|
+
|
|
7
|
+
### `HttpClientError`
|
|
8
|
+
|
|
9
|
+
Base class for all HTTP client errors:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
if (error instanceof HttpClientError) {
|
|
13
|
+
console.log(error.name) // "HttpClientError"
|
|
14
|
+
console.log(error.message) // Error message
|
|
15
|
+
console.log(error.context) // { operation: string }
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `TimeoutError`
|
|
20
|
+
|
|
21
|
+
Request exceeded the timeout:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const result = await api.users.get({ params: { id: '123' }, timeout: 1000 })
|
|
25
|
+
|
|
26
|
+
if (result instanceof TimeoutError) {
|
|
27
|
+
console.log(result.kind) // "TimeoutError"
|
|
28
|
+
console.log(result.context.operation) // "fetch"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `AbortedError`
|
|
33
|
+
|
|
34
|
+
Request was aborted via `AbortSignal`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const controller = new AbortController()
|
|
38
|
+
const result = await api.users.get({
|
|
39
|
+
params: { id: '123' },
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (result instanceof AbortedError) {
|
|
44
|
+
console.log(result.kind) // "AbortedError"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `NetworkError`
|
|
49
|
+
|
|
50
|
+
Network-level failure (no response received):
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
54
|
+
|
|
55
|
+
if (result instanceof NetworkError) {
|
|
56
|
+
console.log(result.kind) // "NetworkError"
|
|
57
|
+
console.log(result.cause) // Underlying error
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `SerializationError`
|
|
62
|
+
|
|
63
|
+
Failed to serialize params, query, or body:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const result = await api.users.create({
|
|
67
|
+
body: { name: '' }, // Fails validation
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (result instanceof SerializationError) {
|
|
71
|
+
console.log(result.kind) // "SerializationError"
|
|
72
|
+
console.log(result.context.operation) // "serialize_body" | "generate_url"
|
|
73
|
+
console.log(result.cause) // Schema validation issues
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `DeserializationError`
|
|
78
|
+
|
|
79
|
+
Failed to parse response:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
83
|
+
|
|
84
|
+
if (result instanceof DeserializationError) {
|
|
85
|
+
console.log(result.kind) // "DeserializationError"
|
|
86
|
+
console.log(result.cause) // Schema validation issues
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `UnexpectedError`
|
|
91
|
+
|
|
92
|
+
Unexpected failure during request:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
96
|
+
|
|
97
|
+
if (result instanceof UnexpectedError) {
|
|
98
|
+
console.log(result.name) // "UnexpectedError"
|
|
99
|
+
console.log(result.context.operation) // "create_request" | "parse_response" | etc.
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Checking Results
|
|
104
|
+
|
|
105
|
+
### Instance Check
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
109
|
+
|
|
110
|
+
if (result instanceof Error) {
|
|
111
|
+
// Handle all error types
|
|
112
|
+
if (result instanceof TimeoutError) {
|
|
113
|
+
// Retry or show timeout message
|
|
114
|
+
} else if (result instanceof NetworkError) {
|
|
115
|
+
// Show network error, maybe retry
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle successful response
|
|
121
|
+
if (result.ok) {
|
|
122
|
+
console.log(result.data)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Error Context
|
|
127
|
+
|
|
128
|
+
All errors have a `context` property with the operation that failed:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
if (result instanceof HttpClientError) {
|
|
132
|
+
result.context.operation
|
|
133
|
+
// "fetch" | "generate_url" | "serialize_body" | "parse_response" | "create_request" | "retry_policy" | "..."
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Raw Response
|
|
138
|
+
|
|
139
|
+
When available, the raw `Response` object is accessible:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const result = await api.users.get({ params: { id: '123' } })
|
|
143
|
+
|
|
144
|
+
if (!result.ok && !(result instanceof Error)) {
|
|
145
|
+
console.log(result.raw_response.status)
|
|
146
|
+
console.log(result.raw_response.headers)
|
|
147
|
+
}
|
|
148
|
+
```
|