@afoures/http-client 0.2.0 → 0.4.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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AbortedError, HttpClientError, NetworkError, ParseError, SerializationError, TimeoutError, UnexpectedError } from "./lib/errors.mjs";
2
2
  import { HTTPFetch, HTTPMethod, HTTPStatus, Parser, Pathname, Schema, Serializer } from "./lib/types.mjs";
3
3
  import { AnyEndpoint, Endpoint, EndpointDefinition } from "./lib/endpoint.mjs";
4
- import { EndpointMap, HttpClientOptions, http_client } from "./lib/http-client.mjs";
5
- export { AbortedError, type AnyEndpoint, Endpoint, type EndpointDefinition, type EndpointMap, type HTTPFetch, type HTTPMethod, type HTTPStatus, HttpClientError, type HttpClientOptions, NetworkError, ParseError, type Parser, type Pathname, type Schema, SerializationError, type Serializer, TimeoutError, UnexpectedError, http_client };
4
+ import { $infer, EndpointMap, HttpClientOptions, http_client } from "./lib/http-client.mjs";
5
+ export { type $infer, AbortedError, type AnyEndpoint, Endpoint, type EndpointDefinition, type EndpointMap, type HTTPFetch, type HTTPMethod, type HTTPStatus, HttpClientError, type HttpClientOptions, NetworkError, ParseError, type Parser, type Pathname, type Schema, SerializationError, type Serializer, TimeoutError, UnexpectedError, http_client };
@@ -33,7 +33,7 @@ declare class Endpoint<http_method extends HTTPMethod.Any, pathname extends Path
33
33
  body: BodyInit | null;
34
34
  content_type?: string;
35
35
  } | SerializationError>;
36
- parse_response(response: Response): Promise<HTTPFetch.ClientErrorResponse<Schema.infer_output<error_schema, string>> | HTTPFetch.ServerErrorResponse<Schema.infer_output<error_schema, string>> | HTTPFetch.SuccessfulResponse<Schema.infer_output<data_schema, void>> | HTTPFetch.RedirectMessage | ParseError>;
36
+ parse_response(raw_response: Response): Promise<HTTPFetch.ClientErrorResponse<Schema.infer_output<error_schema, string>> | HTTPFetch.ServerErrorResponse<Schema.infer_output<error_schema, string>> | HTTPFetch.SuccessfulResponse<Schema.infer_output<data_schema, void>> | HTTPFetch.RedirectMessage | ParseError>;
37
37
  }
38
38
  type AnyEndpoint = Endpoint<any, any, any, any, any, any, any>;
39
39
  //#endregion
@@ -3,6 +3,77 @@ import "./types.mjs";
3
3
  import { RoutePattern } from "@remix-run/route-pattern";
4
4
 
5
5
  //#region src/lib/endpoint.ts
6
+ const RESPONSE = {
7
+ success(method, data, raw_response) {
8
+ const response = {
9
+ ok: true,
10
+ method,
11
+ url: raw_response.url,
12
+ status: raw_response.status,
13
+ data,
14
+ headers: raw_response.headers,
15
+ raw_response
16
+ };
17
+ Object.defineProperty(response, "raw_response", {
18
+ enumerable: false,
19
+ writable: false,
20
+ configurable: false
21
+ });
22
+ return response;
23
+ },
24
+ redirect(method, raw_response) {
25
+ const redirect_to = raw_response.headers.get("Location") || null;
26
+ const response = {
27
+ ok: false,
28
+ method,
29
+ url: raw_response.url,
30
+ status: raw_response.status,
31
+ redirect_to,
32
+ headers: raw_response.headers,
33
+ raw_response
34
+ };
35
+ Object.defineProperty(response, "raw_response", {
36
+ enumerable: false,
37
+ writable: false,
38
+ configurable: false
39
+ });
40
+ return response;
41
+ },
42
+ client_error(method, error, raw_response) {
43
+ const response = {
44
+ ok: false,
45
+ method,
46
+ url: raw_response.url,
47
+ status: raw_response.status,
48
+ error,
49
+ headers: raw_response.headers,
50
+ raw_response
51
+ };
52
+ Object.defineProperty(response, "raw_response", {
53
+ enumerable: false,
54
+ writable: false,
55
+ configurable: false
56
+ });
57
+ return response;
58
+ },
59
+ server_error(method, error, raw_response) {
60
+ const response = {
61
+ ok: false,
62
+ method,
63
+ url: raw_response.url,
64
+ status: raw_response.status,
65
+ error,
66
+ headers: raw_response.headers,
67
+ raw_response
68
+ };
69
+ Object.defineProperty(response, "raw_response", {
70
+ enumerable: false,
71
+ writable: false,
72
+ configurable: false
73
+ });
74
+ return response;
75
+ }
76
+ };
6
77
  var Endpoint = class {
7
78
  #method;
8
79
  #pattern;
@@ -35,7 +106,8 @@ var Endpoint = class {
35
106
  const result = await this.#serializers.params.schema["~standard"].validate(init.params);
36
107
  if (result.issues !== void 0) return new SerializationError("Params serialization failed", {
37
108
  operation: "generate_url",
38
- cause: result.issues
109
+ cause: result.issues,
110
+ input: { params: init.params }
39
111
  });
40
112
  const transformed_params = result.value;
41
113
  if (this.#serializers.params.serialize) pathname_params = this.#serializers.params.serialize(transformed_params);
@@ -47,7 +119,8 @@ var Endpoint = class {
47
119
  const result = await this.#serializers.query.schema["~standard"].validate(init.query);
48
120
  if (result.issues !== void 0) return new SerializationError("Query serialization failed", {
49
121
  cause: result.issues,
50
- operation: "generate_url"
122
+ operation: "generate_url",
123
+ input: { query: init.query }
51
124
  });
52
125
  const transformed_query = result.value;
53
126
  if (typeof this.#serializers.query.serialize === "function") search_params = this.#serializers.query.serialize(transformed_query);
@@ -80,7 +153,8 @@ var Endpoint = class {
80
153
  const result = await this.#serializers.body.schema["~standard"].validate(init.body);
81
154
  if (result.issues !== void 0) return new SerializationError("Body serialization failed", {
82
155
  operation: "serialize_body",
83
- cause: result.issues
156
+ cause: result.issues,
157
+ input: { body: init.body }
84
158
  });
85
159
  const transformed_content = result.value;
86
160
  if (typeof this.#serializers.body.serialize === "function") return this.#serializers.body.serialize(transformed_content);
@@ -89,74 +163,51 @@ var Endpoint = class {
89
163
  content_type: "application/json"
90
164
  };
91
165
  }
92
- async parse_response(response) {
93
- const raw_response = response;
94
- const cloned_response = response.clone();
95
- const status = cloned_response.status;
96
- const headers = cloned_response.headers;
97
- if (status >= 300 && status < 400) return {
98
- ok: false,
99
- status,
100
- redirect_to: headers.get("Location") || null,
101
- headers,
102
- raw_response
103
- };
104
- if (status >= 400 && status < 600) {
166
+ async parse_response(raw_response) {
167
+ const response = raw_response.clone();
168
+ if (raw_response.status >= 300 && raw_response.status < 400) return RESPONSE.redirect(this.#method, raw_response);
169
+ if (raw_response.status >= 400 && raw_response.status < 600) {
105
170
  let error;
106
171
  if (this.#parsers.error) {
107
172
  const parser = this.#parsers.error;
108
173
  let parsed;
109
- if (typeof parser.parse === "function") parsed = await parser.parse(cloned_response.body);
110
- else if (parser.parse === "json") parsed = await parse_as_json(cloned_response);
111
- else if (parser.parse === "text") parsed = await cloned_response.text();
174
+ if (typeof parser.parse === "function") parsed = await parser.parse(response.body);
175
+ else if (parser.parse === "json") parsed = await parse_as_json(response);
176
+ else if (parser.parse === "text") parsed = await response.text();
112
177
  const result = await parser.schema["~standard"].validate(parsed);
113
178
  if (result.issues !== void 0) return new ParseError("Error parsing failed", {
114
179
  cause: result.issues,
115
- operation: "parse_response"
180
+ operation: "parse_response",
181
+ response: {
182
+ status: raw_response.status,
183
+ headers: raw_response.headers,
184
+ body: parsed
185
+ }
116
186
  });
117
187
  error = result.value;
118
- } else error = await cloned_response.text();
119
- return {
120
- ok: false,
121
- status,
122
- error,
123
- headers,
124
- raw_response
125
- };
188
+ } else error = await response.text();
189
+ return raw_response.status >= 400 && raw_response.status < 500 ? RESPONSE.client_error(this.#method, error, raw_response) : RESPONSE.server_error(this.#method, error, raw_response);
126
190
  }
127
- if (status >= 200 && status < 300) {
128
- if (status === 204) return {
129
- ok: true,
130
- status: 204,
131
- data: null,
132
- headers,
133
- raw_response
134
- };
191
+ if (raw_response.status >= 200 && raw_response.status < 300) {
192
+ if (raw_response.status === 204) return RESPONSE.success(this.#method, null, raw_response);
135
193
  if (this.#parsers.data) {
136
194
  const parser = this.#parsers.data;
137
195
  let parsed;
138
- if (typeof parser.parse === "function") parsed = await parser.parse(cloned_response.body);
139
- else if (parser.parse === "json") parsed = await parse_as_json(cloned_response);
140
- else if (parser.parse === "text") parsed = await cloned_response.text();
196
+ if (typeof parser.parse === "function") parsed = await parser.parse(response.body);
197
+ else if (parser.parse === "json") parsed = await parse_as_json(response);
198
+ else if (parser.parse === "text") parsed = await response.text();
141
199
  const result = await parser.schema["~standard"].validate(parsed);
142
200
  if (result.issues !== void 0) return new ParseError("Response parsing failed", {
143
201
  cause: result.issues,
144
- operation: "parse_response"
202
+ operation: "parse_response",
203
+ response: {
204
+ status: raw_response.status,
205
+ headers: raw_response.headers,
206
+ body: parsed
207
+ }
145
208
  });
146
- return {
147
- ok: true,
148
- status,
149
- data: result.value,
150
- headers,
151
- raw_response
152
- };
153
- } else return {
154
- ok: true,
155
- status,
156
- data: null,
157
- headers,
158
- raw_response
159
- };
209
+ return RESPONSE.success(this.#method, result.value, raw_response);
210
+ } else return RESPONSE.success(this.#method, null, raw_response);
160
211
  }
161
212
  throw new Error(`Unhandled status code: ${status}`);
162
213
  }
@@ -1,54 +1,78 @@
1
1
  //#region src/lib/errors.d.ts
2
- type BaseContext = {
3
- operation: string;
2
+ type RequestContext = {
3
+ url: string;
4
+ method: string;
5
+ pathname?: string;
6
+ baseUrl?: string;
7
+ headers?: Headers;
8
+ timeout?: number;
9
+ };
10
+ type ResponseContext = {
11
+ status: number;
12
+ statusText?: string;
13
+ headers?: Headers;
14
+ body?: unknown;
15
+ };
16
+ type TimingContext = {
17
+ startTime?: number;
18
+ duration?: number;
19
+ attempt?: number;
20
+ maxAttempts?: number;
4
21
  };
5
- type ErrorCause = {
6
- cause?: unknown;
22
+ type InputContext = {
23
+ params?: unknown;
24
+ query?: unknown;
25
+ body?: unknown;
26
+ };
27
+ type ErrorContext = {
28
+ operation: string;
29
+ request?: RequestContext;
30
+ response?: ResponseContext;
31
+ timing?: TimingContext;
32
+ input?: InputContext;
7
33
  };
8
34
  declare class HttpClientError extends Error {
9
- constructor(message: string, options: ErrorCause);
10
- }
11
- declare class TimeoutError extends HttpClientError {
12
- readonly context: BaseContext;
35
+ readonly context: ErrorContext;
13
36
  constructor(message: string, {
14
37
  cause,
15
- ...context
16
- }: ErrorCause & BaseContext);
38
+ ...options
39
+ }: {
40
+ cause?: unknown;
41
+ } & Partial<ErrorContext>);
42
+ }
43
+ declare class TimeoutError extends HttpClientError {
44
+ constructor(message: string, options: {
45
+ cause?: unknown;
46
+ } & Partial<ErrorContext>);
17
47
  }
18
48
  declare class AbortedError extends HttpClientError {
19
- readonly context: BaseContext;
20
- constructor(message: string, {
21
- cause,
22
- ...context
23
- }: ErrorCause & BaseContext);
49
+ constructor(message: string, options: {
50
+ cause?: unknown;
51
+ } & Partial<ErrorContext>);
24
52
  }
25
53
  declare class SerializationError extends HttpClientError {
26
- readonly context: BaseContext;
27
- constructor(message: string, {
28
- cause,
29
- ...context
30
- }: ErrorCause & BaseContext);
54
+ constructor(message: string, options: {
55
+ cause?: unknown;
56
+ } & Partial<ErrorContext>);
31
57
  }
32
58
  declare class ParseError extends HttpClientError {
33
- readonly context: BaseContext;
34
- constructor(message: string, {
35
- cause,
36
- ...context
37
- }: ErrorCause & BaseContext);
59
+ constructor(message: string, options: {
60
+ cause?: unknown;
61
+ } & Partial<ErrorContext>);
38
62
  }
39
63
  declare class NetworkError extends HttpClientError {
40
- readonly context: BaseContext;
41
- constructor(message: string, {
42
- cause,
43
- ...context
44
- }: ErrorCause & BaseContext);
64
+ constructor(message: string, options: {
65
+ cause?: unknown;
66
+ } & Partial<ErrorContext>);
45
67
  }
46
68
  declare class UnexpectedError extends Error {
47
- readonly context: BaseContext;
69
+ readonly context: ErrorContext;
48
70
  constructor(message: string, {
49
71
  cause,
50
- ...context
51
- }: ErrorCause & BaseContext);
72
+ ...options
73
+ }: {
74
+ cause?: unknown;
75
+ } & Partial<ErrorContext>);
52
76
  }
53
77
  //#endregion
54
78
  export { AbortedError, HttpClientError, NetworkError, ParseError, SerializationError, TimeoutError, UnexpectedError };
@@ -1,56 +1,54 @@
1
1
  //#region src/lib/errors.ts
2
2
  var HttpClientError = class extends Error {
3
- constructor(message, options) {
4
- super(message, options);
3
+ context;
4
+ constructor(message, { cause, ...options }) {
5
+ super(message, { cause });
5
6
  this.name = "HttpClientError";
7
+ this.context = {
8
+ ...options,
9
+ operation: options.operation ?? "unknown"
10
+ };
6
11
  }
7
12
  };
8
13
  var TimeoutError = class extends HttpClientError {
9
- context;
10
- constructor(message, { cause, ...context }) {
11
- super(message, { cause });
14
+ constructor(message, options) {
15
+ super(message, options);
12
16
  this.name = "TimeoutError";
13
- this.context = context;
14
17
  }
15
18
  };
16
19
  var AbortedError = class extends HttpClientError {
17
- context;
18
- constructor(message, { cause, ...context }) {
19
- super(message, { cause });
20
+ constructor(message, options) {
21
+ super(message, options);
20
22
  this.name = "AbortedError";
21
- this.context = context;
22
23
  }
23
24
  };
24
25
  var SerializationError = class extends HttpClientError {
25
- context;
26
- constructor(message, { cause, ...context }) {
27
- super(message, { cause });
26
+ constructor(message, options) {
27
+ super(message, options);
28
28
  this.name = "SerializationError";
29
- this.context = context;
30
29
  }
31
30
  };
32
31
  var ParseError = class extends HttpClientError {
33
- context;
34
- constructor(message, { cause, ...context }) {
35
- super(message, { cause });
32
+ constructor(message, options) {
33
+ super(message, options);
36
34
  this.name = "ParseError";
37
- this.context = context;
38
35
  }
39
36
  };
40
37
  var NetworkError = class extends HttpClientError {
41
- context;
42
- constructor(message, { cause, ...context }) {
43
- super(message, { cause });
38
+ constructor(message, options) {
39
+ super(message, options);
44
40
  this.name = "NetworkError";
45
- this.context = context;
46
41
  }
47
42
  };
48
43
  var UnexpectedError = class extends Error {
49
44
  context;
50
- constructor(message, { cause, ...context }) {
45
+ constructor(message, { cause, ...options }) {
51
46
  super(message, { cause });
52
47
  this.name = "UnexpectedError";
53
- this.context = context;
48
+ this.context = {
49
+ ...options,
50
+ operation: options.operation ?? "unknown"
51
+ };
54
52
  }
55
53
  };
56
54
 
@@ -1,5 +1,5 @@
1
1
  import { AbortedError, NetworkError, ParseError, SerializationError, TimeoutError, UnexpectedError } from "./errors.mjs";
2
- import { HTTPFetch, HTTPMethod, HeadersInitWithReducer, MaybePromise, Schema } from "./types.mjs";
2
+ import { HTTPFetch, HTTPMethod, HeadersInitWithReducer, MaybePromise, Pathname, Pretty, Schema } from "./types.mjs";
3
3
  import { AnyEndpoint, Endpoint } from "./endpoint.mjs";
4
4
  import * as _standard_schema_spec0 from "@standard-schema/spec";
5
5
 
@@ -8,6 +8,23 @@ interface EndpointMap {
8
8
  [name: string]: AnyEndpoint | EndpointMap;
9
9
  }
10
10
  type CustomFetch = (request: Request) => Promise<Response>;
11
+ type Hooks = {
12
+ on_request?: (request: Request) => void;
13
+ on_response?: (response: Response) => void;
14
+ };
15
+ declare function fetch_endpoint_factory<http_method extends HTTPMethod.Any, pathname extends Pathname.Relative, params_schema extends Schema._, query_schema extends Schema._, body_schema extends Schema._, data_schema extends Schema._, error_schema extends Schema._>({
16
+ base_url,
17
+ endpoint,
18
+ custom_fetch,
19
+ get_default_options,
20
+ hooks
21
+ }: {
22
+ base_url: string;
23
+ endpoint: Endpoint<http_method, pathname, params_schema, query_schema, body_schema, data_schema, error_schema>;
24
+ custom_fetch: CustomFetch;
25
+ get_default_options?: () => MaybePromise<HTTPFetch.OptionalRequestInit & HTTPFetch.DefaultRequestInit>;
26
+ hooks?: Hooks;
27
+ }): (input: Pretty<HTTPFetch.TypedParamsInit<pathname, params_schema> & HTTPFetch.TypedQueryInit<query_schema> & HTTPFetch.TypedBodyInit<body_schema> & HTTPFetch.OptionalRequestInit & HTTPFetch.DefaultRequestInit>) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>>;
11
28
  type HttpClientOptions<endpoints extends EndpointMap> = {
12
29
  base_url: string;
13
30
  endpoints: endpoints;
@@ -60,5 +77,19 @@ declare function http_client<const endpoints extends EndpointMap>({
60
77
  } & Omit<RequestInit, "body" | "method" | "headers"> extends infer T_16 ? { [K_10 in keyof T_16]: T_16[K_10] } : never) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>> : T_15[name_9] extends EndpointMap ? (T_15[name_9] extends infer T_17 extends EndpointMap ? { -readonly [name_10 in keyof T_17]: T_17[name_10] extends Endpoint<infer http_method extends HTTPMethod.Any, infer pathname extends `/${string}`, infer params_schema extends Schema._<unknown, unknown>, infer query_schema extends Schema._<unknown, unknown>, infer body_schema extends Schema._<unknown, unknown>, infer data_schema extends Schema._<unknown, unknown>, infer error_schema extends Schema._<unknown, unknown>> ? (input: HTTPFetch.TypedParamsInit<pathname, params_schema> & HTTPFetch.TypedQueryInit<query_schema> & HTTPFetch.TypedBodyInit<body_schema> & HTTPFetch.OptionalRequestInit & {
61
78
  headers?: HeadersInitWithReducer;
62
79
  } & Omit<RequestInit, "body" | "method" | "headers"> extends infer T_18 ? { [K_11 in keyof T_18]: T_18[K_11] } : never) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>> : T_17[name_10] extends EndpointMap ? (T_17[name_10] extends infer T_19 extends EndpointMap ? { -readonly [name_11 in keyof T_19]: T_19[name_11] extends Endpoint<infer http_method extends HTTPMethod.Any, infer pathname extends `/${string}`, infer params_schema extends Schema._<unknown, unknown>, infer query_schema extends Schema._<unknown, unknown>, infer body_schema extends Schema._<unknown, unknown>, infer data_schema extends Schema._<unknown, unknown>, infer error_schema extends Schema._<unknown, unknown>> ? (input: /*elided*/any) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>> : T_19[name_11] extends EndpointMap ? /*elided*/any : never } : never) extends infer T_18 ? { [K_11 in keyof T_18]: T_18[K_11] } : never : never } : never) extends infer T_16 ? { [K_10 in keyof T_16]: T_16[K_10] } : never : never } : never) extends infer T_14 ? { [K_9 in keyof T_14]: T_14[K_9] } : never : never } : never) extends infer T_12 ? { [K_8 in keyof T_12]: T_12[K_8] } : never : never } : never) extends infer T_10 ? { [K_7 in keyof T_10]: T_10[K_7] } : never : never } : never) extends infer T_8 ? { [K_6 in keyof T_8]: T_8[K_6] } : never : never } : never) extends infer T_6 ? { [K_5 in keyof T_6]: T_6[K_5] } : never : never } : never) extends infer T_4 ? { [K_4 in keyof T_4]: T_4[K_4] } : never : never } : never) extends infer T_2 ? { [K_3 in keyof T_2]: T_2[K_3] } : never : never } : never) extends infer T ? { [K_2 in keyof T]: T[K_2] } : never : never }[K] };
80
+ type AnyFetchEndpointFunction = ReturnType<typeof fetch_endpoint_factory<any, any, any, any, any, any, any>>;
81
+ declare namespace $infer {
82
+ type Query<fetch_endpoint extends AnyFetchEndpointFunction> = Parameters<fetch_endpoint>[0] extends {
83
+ query: infer query;
84
+ } ? query : never;
85
+ type Params<fetch_endpoint extends AnyFetchEndpointFunction> = Parameters<fetch_endpoint>[0] extends {
86
+ params: infer params;
87
+ } ? params : never;
88
+ type Body<fetch_endpoint extends AnyFetchEndpointFunction> = Parameters<fetch_endpoint>[0] extends {
89
+ body: infer body;
90
+ } ? body : never;
91
+ type Data<fetch_endpoint extends AnyFetchEndpointFunction> = ReturnType<fetch_endpoint> extends HTTPFetch.SuccessfulResponse<infer data> ? data : never;
92
+ type Error<fetch_endpoint extends AnyFetchEndpointFunction> = ReturnType<fetch_endpoint> extends HTTPFetch.ClientErrorResponse<infer error> ? error : ReturnType<fetch_endpoint> extends HTTPFetch.ServerErrorResponse<infer error> ? error : never;
93
+ }
63
94
  //#endregion
64
- export { EndpointMap, HttpClientOptions, http_client };
95
+ export { $infer, EndpointMap, HttpClientOptions, http_client };
@@ -6,7 +6,16 @@ import { extract_args, merge_options, remove_custom_options, sleep } from "./uti
6
6
  //#region src/lib/http-client.ts
7
7
  function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_options = () => ({}), hooks = {} }) {
8
8
  async function fetch_endpoint(input) {
9
- if (!URL.canParse(base_url)) return new UnexpectedError(`Invalid base_url: ${base_url}`, { operation: "base_url_validation" });
9
+ let start_time = Date.now();
10
+ if (!URL.canParse(base_url)) return new UnexpectedError(`Invalid base_url: ${base_url}`, {
11
+ operation: "base_url_validation",
12
+ request: {
13
+ url: base_url,
14
+ method: endpoint.method,
15
+ baseUrl: base_url
16
+ },
17
+ timing: { startTime: start_time }
18
+ });
10
19
  const { args, options } = extract_args(input);
11
20
  const { headers, ...merged_options } = merge_options(await get_default_options(), endpoint.options, options);
12
21
  const url = await endpoint.generate_url({
@@ -15,12 +24,29 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
15
24
  query: args.query
16
25
  }).catch((error) => new UnexpectedError("Failed to generate URL", {
17
26
  cause: error,
18
- operation: "generate_url"
27
+ operation: "generate_url",
28
+ request: {
29
+ url: base_url,
30
+ method: endpoint.method,
31
+ baseUrl: base_url
32
+ },
33
+ input: {
34
+ params: args.params,
35
+ query: args.query
36
+ },
37
+ timing: { startTime: start_time }
19
38
  }));
20
39
  if (url instanceof Error) return url;
21
40
  const serialized = await endpoint.serialize_body({ body: args.body }).catch((error) => new UnexpectedError("Failed to serialize body", {
22
41
  cause: error,
23
- operation: "serialize_body"
42
+ operation: "serialize_body",
43
+ request: {
44
+ url: url instanceof URL ? url.toString() : base_url,
45
+ method: endpoint.method,
46
+ baseUrl: base_url
47
+ },
48
+ input: { body: args.body },
49
+ timing: { startTime: start_time }
24
50
  }));
25
51
  if (serialized instanceof Error) return serialized;
26
52
  headers.delete("Content-Type");
@@ -50,7 +76,18 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
50
76
  } catch (local_error) {
51
77
  error = new UnexpectedError("Failed to create request", {
52
78
  cause: local_error,
53
- operation: "create_request"
79
+ operation: "create_request",
80
+ request: {
81
+ url: url instanceof URL ? url.toString() : base_url,
82
+ method: endpoint.method,
83
+ headers,
84
+ timeout: options.timeout,
85
+ baseUrl: base_url
86
+ },
87
+ timing: {
88
+ startTime: start_time,
89
+ attempt: 1
90
+ }
54
91
  });
55
92
  break;
56
93
  }
@@ -60,17 +97,48 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
60
97
  response = await custom_fetch(request);
61
98
  error = void 0;
62
99
  } catch (local_error) {
100
+ const duration = Date.now() - start_time;
63
101
  if (local_error instanceof Error && local_error.name === "TimeoutError") error = new TimeoutError(local_error.message, {
64
102
  cause: local_error,
65
- operation: "fetch"
103
+ operation: "fetch",
104
+ request: {
105
+ url: request.url,
106
+ method: request.method,
107
+ timeout: options.timeout
108
+ },
109
+ timing: {
110
+ startTime: start_time,
111
+ duration,
112
+ attempt
113
+ }
66
114
  });
67
115
  else if (local_error instanceof Error && local_error.name === "AbortError") error = new AbortedError(local_error.message, {
68
116
  cause: local_error,
69
- operation: "fetch"
117
+ operation: "fetch",
118
+ request: {
119
+ url: request.url,
120
+ method: request.method,
121
+ timeout: options.timeout
122
+ },
123
+ timing: {
124
+ startTime: start_time,
125
+ duration,
126
+ attempt
127
+ }
70
128
  });
71
129
  else error = new NetworkError("Network error", {
72
130
  cause: local_error,
73
- operation: "fetch"
131
+ operation: "fetch",
132
+ request: {
133
+ url: request.url,
134
+ method: request.method,
135
+ timeout: options.timeout
136
+ },
137
+ timing: {
138
+ startTime: start_time,
139
+ duration,
140
+ attempt
141
+ }
74
142
  });
75
143
  }
76
144
  try {
@@ -91,25 +159,77 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
91
159
  } catch (local_error) {
92
160
  error = new UnexpectedError("Failed to check retry policy", {
93
161
  cause: local_error,
94
- operation: "retry_policy"
162
+ operation: "retry_policy",
163
+ request: {
164
+ url: url instanceof URL ? url.toString() : base_url,
165
+ method: endpoint.method,
166
+ timeout: options.timeout,
167
+ baseUrl: base_url
168
+ },
169
+ timing: {
170
+ startTime: start_time,
171
+ attempt,
172
+ maxAttempts: typeof retry_policy.attempts === "function" ? void 0 : retry_policy.attempts
173
+ }
95
174
  });
96
175
  break;
97
176
  }
98
177
  } while (true);
99
178
  if (error) return error;
100
- if (!response) return new UnexpectedError("", {
179
+ if (!response) return new UnexpectedError("No response received", {
101
180
  cause: "No response received",
102
- operation: "parse_response"
181
+ operation: "parse_response",
182
+ request: {
183
+ url: url instanceof URL ? url.toString() : base_url,
184
+ method: endpoint.method,
185
+ timeout: options.timeout,
186
+ baseUrl: base_url
187
+ },
188
+ timing: {
189
+ startTime: start_time,
190
+ attempt
191
+ }
103
192
  });
104
193
  hooks.on_response?.(response);
105
- return await endpoint.parse_response(response).catch((error) => {
194
+ return await endpoint.parse_response(response).catch(async (error) => {
195
+ const response_body = await response.clone().text().catch(() => void 0);
106
196
  if (error instanceof Error && error.name === "AbortError") return new AbortedError(error.message, {
107
197
  cause: error,
108
- operation: "parse_response"
198
+ operation: "parse_response",
199
+ request: {
200
+ url: response.url,
201
+ method: request?.method
202
+ },
203
+ response: {
204
+ status: response.status,
205
+ statusText: response.statusText,
206
+ headers: response.headers,
207
+ body: response_body
208
+ },
209
+ timing: {
210
+ startTime: start_time,
211
+ attempt
212
+ }
109
213
  });
110
214
  return new UnexpectedError("Failed to parse response", {
111
215
  cause: error,
112
- operation: "parse_response"
216
+ operation: "parse_response",
217
+ request: {
218
+ url: response.url,
219
+ method: request?.method,
220
+ timeout: options.timeout,
221
+ baseUrl: base_url
222
+ },
223
+ response: {
224
+ status: response.status,
225
+ statusText: response.statusText,
226
+ headers: response.headers,
227
+ body: response_body
228
+ },
229
+ timing: {
230
+ startTime: start_time,
231
+ attempt
232
+ }
113
233
  });
114
234
  });
115
235
  }
@@ -63,6 +63,8 @@ declare namespace RetryPolicy {
63
63
  }
64
64
  declare namespace HTTPFetch {
65
65
  type SharedResponseContent = {
66
+ method: HTTPMethod.Any;
67
+ url?: string;
66
68
  headers: Headers;
67
69
  raw_response: Response;
68
70
  };
@@ -145,3 +145,46 @@ if (result.ok) {
145
145
  console.log(result.error)
146
146
  }
147
147
  ```
148
+
149
+ ## Type Inference
150
+
151
+ The `$infer` namespace provides helpers to extract types from endpoint functions:
152
+
153
+ ```typescript
154
+ import { $infer, http_client, Endpoint } from '@afoures/http-client'
155
+ import { z } from 'zod'
156
+
157
+ const api = http_client({
158
+ base_url: 'https://api.example.com',
159
+ endpoints: {
160
+ users: {
161
+ list: new Endpoint({
162
+ method: 'GET',
163
+ pathname: '/users',
164
+ data: { schema: z.array(z.object({ id: z.string(), name: z.string() })), parse: 'json' },
165
+ }),
166
+ create: new Endpoint({
167
+ method: 'POST',
168
+ pathname: '/users',
169
+ body: { schema: z.object({ name: z.string() }) },
170
+ data: { schema: z.object({ id: z.string(), name: z.string() }), parse: 'json' },
171
+ }),
172
+ },
173
+ },
174
+ })
175
+
176
+ type UsersListQuery = $infer.Query<typeof api.users.list>
177
+ type UsersListData = $infer.Data<typeof api.users.list>
178
+ type UsersListParams = $infer.Params<typeof api.users.list>
179
+ type UsersListError = $infer.Error<typeof api.users.list>
180
+
181
+ type UsersCreateBody = $infer.Body<typeof api.users.create>
182
+ type UsersCreateData = $infer.Data<typeof api.users.create>
183
+ ```
184
+
185
+ Available type helpers:
186
+ - `$infer.Query` - Extracts the query parameter type
187
+ - `$infer.Params` - Extracts the URL params type
188
+ - `$infer.Body` - Extracts the request body type
189
+ - `$infer.Data` - Extracts the successful response data type
190
+ - `$infer.Error` - Extracts the error response type (client or server errors)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afoures/http-client",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A typesafe and robust HTTP client",
5
5
  "homepage": "https://github.com/afoures/http-client#readme",
6
6
  "bugs": {
@@ -35,12 +35,12 @@
35
35
  "@standard-schema/spec": "^1.1.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@afoures/auto-release": "^0.4.1",
38
+ "@afoures/auto-release": "^0.5.0",
39
39
  "@arktype/attest": "^0.56.0",
40
- "@types/node": "^24.12.0",
41
- "msw": "^2.12.10",
40
+ "@types/node": "^24.12.2",
41
+ "msw": "^2.13.2",
42
42
  "oxfmt": "^0.36.0",
43
- "oxlint": "^1.51.0",
43
+ "oxlint": "^1.59.0",
44
44
  "tsdown": "^0.21.0",
45
45
  "typescript": "^5.9.3",
46
46
  "zod": "^4.3.6"