@monarcode/effetch 0.0.0-stage → 0.1.0-alpha.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.
@@ -0,0 +1,367 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Headers from "effect/http/Headers";
3
+ import * as HttpClient from "effect/http/HttpClient";
4
+ import * as Schema from "effect/Schema";
5
+
6
+ import { NetworkError } from "./Errors.js";
7
+ import {
8
+ AnySchema,
9
+ BodyMethod,
10
+ NoBodyMethod,
11
+ RequestMethod,
12
+ } from "./Options.js";
13
+ import { Plugin } from "./Plugin.js";
14
+ //#region src/Client.d.ts
15
+ /**
16
+ * Configuration for {@link make}.
17
+ *
18
+ * - `baseUrl`: prepended to every path that isn't already an absolute
19
+ * `http://` or `https://` URL. A string, or an Effect (a `Config` works)
20
+ * that's run once, when the client is made. It can't contain a query
21
+ * string or a fragment: pass those with each call's `query` option.
22
+ * - `headers`: sent with every request. A header passed to a call replaces
23
+ * the client's one of the same name. `content-type` and `content-length`
24
+ * are ignored here: they come from each request's body (set
25
+ * `content-type` on a call to override it). When a call has an `output`
26
+ * schema, effetch sends `Accept: application/json` unless an `accept`
27
+ * header is set here or on the call.
28
+ * - `error`: the default schema for non-2xx response bodies. A call's own
29
+ * `error` option replaces it.
30
+ * - `plugins`: hooks into every call (see {@link Plugin}). `plugins[0]` is
31
+ * the outermost layer: its `onRequest` runs first and its `onResponse`
32
+ * last. Names must be unique. Pass the list inline: annotating it as
33
+ * `Plugin.Any[]` erases the plugins' types.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import * as Config from "effect/Config";
38
+ * import * as Effect from "effect/Effect";
39
+ * import * as HttpClientRequest from "effect/http/HttpClientRequest";
40
+ * import * as Schema from "effect/Schema";
41
+ * import * as Fetch from "@monarcode/effetch";
42
+ *
43
+ * const ApiError = Schema.Struct({ code: Schema.String });
44
+ *
45
+ * const apiKey = Fetch.plugin({
46
+ * name: "apiKey",
47
+ * onRequest: (request) =>
48
+ * Effect.succeed(HttpClientRequest.setHeader(request, "x-api-key", "key")),
49
+ * });
50
+ *
51
+ * const make = Fetch.make({
52
+ * baseUrl: Config.String("API_URL"),
53
+ * headers: { accept: "application/json" },
54
+ * error: ApiError,
55
+ * plugins: [apiKey],
56
+ * });
57
+ * ```
58
+ *
59
+ * @since 0.1.0
60
+ */
61
+ export interface ClientConfig<
62
+ Ps extends ReadonlyArray<Plugin.Any> = [],
63
+ EB extends AnySchema = Schema.Unknown,
64
+ BaseE = never,
65
+ BaseR = never,
66
+ > {
67
+ /** Prepended to relative paths. A string, or an Effect run once by `make`. */
68
+ readonly baseUrl?: string | Effect.Effect<string, BaseE, BaseR> | undefined;
69
+ /** Headers sent with every request. */
70
+ readonly headers?: Headers.Input | undefined;
71
+ /** Default schema for non-2xx response bodies. */
72
+ readonly error?: EB | undefined;
73
+ /** Plugins, outermost first. Names must be unique. */
74
+ readonly plugins?: Ps | undefined;
75
+ }
76
+ /**
77
+ * A client made by {@link make}. Every call returns an Effect that needs
78
+ * no services, apart from those the call's own schemas require, and that
79
+ * can fail with the plugins' errors on top of effetch's own.
80
+ *
81
+ * - `Ps`: the plugins the client was made with.
82
+ * - `EB`: the client's default `error` schema.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import * as Effect from "effect/Effect";
87
+ * import * as Schema from "effect/Schema";
88
+ * import * as Fetch from "@monarcode/effetch";
89
+ *
90
+ * const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String });
91
+ *
92
+ * const program = (api: Fetch.Client) =>
93
+ * api.get("/todos/:id", { params: { id: 1 }, output: Todo }).pipe(
94
+ * Effect.catchTag("HttpError", (error) =>
95
+ * error.status === 404 ? Effect.succeed(undefined) : Effect.fail(error),
96
+ * ),
97
+ * );
98
+ * ```
99
+ *
100
+ * @since 0.1.0
101
+ */
102
+ export interface Client<
103
+ Ps extends ReadonlyArray<Plugin.Any> = [],
104
+ EB extends AnySchema = Schema.Unknown,
105
+ > {
106
+ /**
107
+ * The configured `HttpClient` underneath. It adds the base URL and the
108
+ * default headers to the requests you give it and runs every plugin
109
+ * with its defaults, but does no status check, no decoding and no
110
+ * `onError` hooks.
111
+ *
112
+ * Transport failures are `NetworkError`s, next to the plugins' own
113
+ * errors. Use it for anything the methods don't cover, such as
114
+ * streaming a response.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import * as Effect from "effect/Effect";
119
+ * import * as HttpClientRequest from "effect/http/HttpClientRequest";
120
+ * import * as Fetch from "@monarcode/effetch";
121
+ *
122
+ * const download = (api: Fetch.Client) =>
123
+ * Effect.flatMap(
124
+ * api.httpClient.execute(
125
+ * HttpClientRequest.get("https://api.example.com/export.csv"),
126
+ * ),
127
+ * (response) => response.text,
128
+ * );
129
+ * ```
130
+ */
131
+ readonly httpClient: HttpClient.HttpClient.With<
132
+ NetworkError | Plugin.Errors<Ps>
133
+ >;
134
+ /**
135
+ * Sends a request with any HTTP method. The other methods are shorthand
136
+ * for this one.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * import * as Schema from "effect/Schema";
141
+ * import * as Fetch from "@monarcode/effetch";
142
+ *
143
+ * const Todo = Schema.Struct({ id: Schema.Number, done: Schema.Boolean });
144
+ *
145
+ * const complete = (api: Fetch.Client, id: number) =>
146
+ * api.request("PATCH", "/todos/:id", {
147
+ * params: { id },
148
+ * body: { done: true },
149
+ * output: Todo,
150
+ * });
151
+ * ```
152
+ */
153
+ readonly request: RequestMethod<Ps, EB>;
154
+ /**
155
+ * Sends a `GET` request. Takes no body.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * import * as Schema from "effect/Schema";
160
+ * import * as Fetch from "@monarcode/effetch";
161
+ *
162
+ * const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String });
163
+ *
164
+ * const list = (api: Fetch.Client) =>
165
+ * api.get("/todos", {
166
+ * query: { page: 1, tags: ["work", "urgent"] },
167
+ * output: Schema.Array(Todo),
168
+ * });
169
+ * // GET /todos?page=1&tags=work&tags=urgent
170
+ *
171
+ * const one = (api: Fetch.Client, id: number) =>
172
+ * api.get("/todos/:id", { params: { id }, output: Todo });
173
+ * ```
174
+ */
175
+ readonly get: NoBodyMethod<Ps, EB>;
176
+ /**
177
+ * Sends a `HEAD` request. Takes no body.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * import * as Effect from "effect/Effect";
182
+ * import * as Fetch from "@monarcode/effetch";
183
+ *
184
+ * const exists = (api: Fetch.Client, id: number) =>
185
+ * api.head("/todos/:id", { params: { id } }).pipe(
186
+ * Effect.as(true),
187
+ * Effect.catchTag("HttpError", () => Effect.succeed(false)),
188
+ * );
189
+ * ```
190
+ */
191
+ readonly head: NoBodyMethod<Ps, EB>;
192
+ /**
193
+ * Sends an `OPTIONS` request. Takes no body.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * import * as Effect from "effect/Effect";
198
+ * import * as Fetch from "@monarcode/effetch";
199
+ *
200
+ * const allowed = (api: Fetch.Client) =>
201
+ * Effect.map(api.options("/todos"), (response) => response.headers["allow"]);
202
+ * ```
203
+ */
204
+ readonly options: NoBodyMethod<Ps, EB>;
205
+ /**
206
+ * Sends a `POST` request. `body` is sent according to its type, or
207
+ * encoded with `input` when given.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * import * as Schema from "effect/Schema";
212
+ * import * as Fetch from "@monarcode/effetch";
213
+ *
214
+ * const NewTodo = Schema.Struct({ title: Schema.String, due: Schema.Date });
215
+ * const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String });
216
+ *
217
+ * const create = (api: Fetch.Client) =>
218
+ * api.post("/todos", {
219
+ * input: NewTodo,
220
+ * body: { title: "Write docs", due: new Date() },
221
+ * output: Todo,
222
+ * });
223
+ *
224
+ * const upload = (api: Fetch.Client, file: Blob) => {
225
+ * const form = new FormData();
226
+ * form.append("file", file);
227
+ * return api.post("/upload", { body: form });
228
+ * };
229
+ * ```
230
+ */
231
+ readonly post: BodyMethod<Ps, EB>;
232
+ /**
233
+ * Sends a `PUT` request. `body` is sent according to its type, or
234
+ * encoded with `input` when given.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * import * as Schema from "effect/Schema";
239
+ * import * as Fetch from "@monarcode/effetch";
240
+ *
241
+ * const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String });
242
+ *
243
+ * const replace = (api: Fetch.Client, todo: typeof Todo.Type) =>
244
+ * api.put("/todos/:id", {
245
+ * params: { id: todo.id },
246
+ * input: Todo,
247
+ * body: todo,
248
+ * output: Todo,
249
+ * });
250
+ * ```
251
+ */
252
+ readonly put: BodyMethod<Ps, EB>;
253
+ /**
254
+ * Sends a `PATCH` request. `body` is sent according to its type, or
255
+ * encoded with `input` when given.
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * import * as Schema from "effect/Schema";
260
+ * import * as Fetch from "@monarcode/effetch";
261
+ *
262
+ * const Todo = Schema.Struct({ id: Schema.Number, done: Schema.Boolean });
263
+ *
264
+ * const complete = (api: Fetch.Client, id: number) =>
265
+ * api.patch("/todos/:id", {
266
+ * params: { id },
267
+ * body: { done: true },
268
+ * output: Todo,
269
+ * });
270
+ * ```
271
+ */
272
+ readonly patch: BodyMethod<Ps, EB>;
273
+ /**
274
+ * Sends a `DELETE` request. Named `del` because `delete` is a reserved
275
+ * word.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * import * as Fetch from "@monarcode/effetch";
280
+ *
281
+ * const remove = (api: Fetch.Client, id: number) =>
282
+ * api.del("/todos/:id", { params: { id } });
283
+ * ```
284
+ */
285
+ readonly del: BodyMethod<Ps, EB>;
286
+ }
287
+ /**
288
+ * Makes a {@link Client}.
289
+ *
290
+ * `make` is an Effect: it needs an `HttpClient` (for example
291
+ * `FetchHttpClient.layer`, or a mock in tests), whatever the `baseUrl`
292
+ * Effect needs, and the services the plugins need. The client holds on to
293
+ * all of them, so its calls need nothing from the environment apart from
294
+ * the services of their own schemas.
295
+ *
296
+ * Calls run with the services and references `make` had, plus those of
297
+ * the calling fiber, which win: a reference provided around one call (such
298
+ * as `FetchHttpClient.RequestInit`) overrides the one `make` saw.
299
+ *
300
+ * Calls fail with `NetworkError` when no response arrives, `HttpError` for
301
+ * a non-2xx status, `DecodeError` when `output` doesn't match, and with
302
+ * the plugins' own errors.
303
+ *
304
+ * Invalid input is a defect, not a typed error, because it's a mistake in
305
+ * the calling code: two plugins with the same name, a missing path param,
306
+ * a body that can't be encoded, a body on a `GET` or `HEAD` request, a
307
+ * `baseUrl` with a query string or a fragment, and, outside browsers, a
308
+ * relative URL without a `baseUrl` (browsers resolve it against the page's
309
+ * URL).
310
+ *
311
+ * An `HttpClient` that fails on non-2xx statuses (`filterStatusOk`) works
312
+ * too: effetch takes the response back and reports the usual `HttpError`.
313
+ *
314
+ * **Browsers and tracing headers.** Effect's `HttpClient` adds `traceparent`
315
+ * and `b3` headers to every request. In a browser, those make cross-origin
316
+ * calls to APIs that don't allow them fail the CORS preflight. Turn that
317
+ * off where you run the program, or only around `make`, which then applies
318
+ * it to all of the client's calls:
319
+ *
320
+ * ```ts
321
+ * import * as Effect from "effect/Effect";
322
+ * import * as HttpClient from "effect/http/HttpClient";
323
+ *
324
+ * declare const program: Effect.Effect<void>;
325
+ *
326
+ * const main = Effect.provideService(
327
+ * program,
328
+ * HttpClient.TracerPropagationEnabled,
329
+ * false,
330
+ * );
331
+ * ```
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * import * as Context from "effect/Context";
336
+ * import * as Effect from "effect/Effect";
337
+ * import * as Layer from "effect/Layer";
338
+ * import * as FetchHttpClient from "effect/http/FetchHttpClient";
339
+ * import * as Fetch from "@monarcode/effetch";
340
+ *
341
+ * const make = Fetch.make({
342
+ * baseUrl: "https://api.example.com",
343
+ * headers: { accept: "application/json" },
344
+ * });
345
+ *
346
+ * class Api extends Context.Service<Api, Effect.Success<typeof make>>()("Api") {}
347
+ *
348
+ * const ApiLive = Layer.effect(Api)(make).pipe(
349
+ * Layer.provide(FetchHttpClient.layer),
350
+ * );
351
+ * ```
352
+ *
353
+ * @since 0.1.0
354
+ */
355
+ export declare const make: <
356
+ const Ps extends ReadonlyArray<Plugin.Any> = [],
357
+ EB extends AnySchema = Schema.Unknown,
358
+ BaseE = never,
359
+ BaseR = never,
360
+ >(
361
+ config?: ClientConfig<Ps, EB, BaseE, BaseR>,
362
+ ) => Effect.Effect<
363
+ Client<Ps, EB>,
364
+ BaseE,
365
+ HttpClient.HttpClient | BaseR | Plugin.Services<Ps>
366
+ >;
367
+ //#endregion
package/dist/Client.js ADDED
@@ -0,0 +1,110 @@
1
+ import { hasQueryOrFragment } from "./internal/url.js";
2
+ import { makeApplyDefaults, mapClientErrors } from "./internal/pipeline.js";
3
+ import { applyPlugins, provideOverrides } from "./internal/plugins.js";
4
+ import { makeCall } from "./internal/request.js";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Headers from "effect/http/Headers";
7
+ import * as HttpClient from "effect/http/HttpClient";
8
+
9
+ //#region src/Client.ts
10
+ /**
11
+ * Makes a {@link Client}.
12
+ *
13
+ * `make` is an Effect: it needs an `HttpClient` (for example
14
+ * `FetchHttpClient.layer`, or a mock in tests), whatever the `baseUrl`
15
+ * Effect needs, and the services the plugins need. The client holds on to
16
+ * all of them, so its calls need nothing from the environment apart from
17
+ * the services of their own schemas.
18
+ *
19
+ * Calls run with the services and references `make` had, plus those of
20
+ * the calling fiber, which win: a reference provided around one call (such
21
+ * as `FetchHttpClient.RequestInit`) overrides the one `make` saw.
22
+ *
23
+ * Calls fail with `NetworkError` when no response arrives, `HttpError` for
24
+ * a non-2xx status, `DecodeError` when `output` doesn't match, and with
25
+ * the plugins' own errors.
26
+ *
27
+ * Invalid input is a defect, not a typed error, because it's a mistake in
28
+ * the calling code: two plugins with the same name, a missing path param,
29
+ * a body that can't be encoded, a body on a `GET` or `HEAD` request, a
30
+ * `baseUrl` with a query string or a fragment, and, outside browsers, a
31
+ * relative URL without a `baseUrl` (browsers resolve it against the page's
32
+ * URL).
33
+ *
34
+ * An `HttpClient` that fails on non-2xx statuses (`filterStatusOk`) works
35
+ * too: effetch takes the response back and reports the usual `HttpError`.
36
+ *
37
+ * **Browsers and tracing headers.** Effect's `HttpClient` adds `traceparent`
38
+ * and `b3` headers to every request. In a browser, those make cross-origin
39
+ * calls to APIs that don't allow them fail the CORS preflight. Turn that
40
+ * off where you run the program, or only around `make`, which then applies
41
+ * it to all of the client's calls:
42
+ *
43
+ * ```ts
44
+ * import * as Effect from "effect/Effect";
45
+ * import * as HttpClient from "effect/http/HttpClient";
46
+ *
47
+ * declare const program: Effect.Effect<void>;
48
+ *
49
+ * const main = Effect.provideService(
50
+ * program,
51
+ * HttpClient.TracerPropagationEnabled,
52
+ * false,
53
+ * );
54
+ * ```
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import * as Context from "effect/Context";
59
+ * import * as Effect from "effect/Effect";
60
+ * import * as Layer from "effect/Layer";
61
+ * import * as FetchHttpClient from "effect/http/FetchHttpClient";
62
+ * import * as Fetch from "@monarcode/effetch";
63
+ *
64
+ * const make = Fetch.make({
65
+ * baseUrl: "https://api.example.com",
66
+ * headers: { accept: "application/json" },
67
+ * });
68
+ *
69
+ * class Api extends Context.Service<Api, Effect.Success<typeof make>>()("Api") {}
70
+ *
71
+ * const ApiLive = Layer.effect(Api)(make).pipe(
72
+ * Layer.provide(FetchHttpClient.layer),
73
+ * );
74
+ * ```
75
+ *
76
+ * @since 0.1.0
77
+ */
78
+ const make = (config = {}) => Effect.gen(function* () {
79
+ const provided = yield* HttpClient.HttpClient;
80
+ const context = yield* Effect.context();
81
+ const plugins = config.plugins ?? [];
82
+ const duplicate = plugins.find((plugin, index) => plugins.findIndex((other) => other.name === plugin.name) !== index);
83
+ if (duplicate !== void 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Two plugins are named "${duplicate.name}"`));
84
+ const baseUrl = config.baseUrl === void 0 || typeof config.baseUrl === "string" ? config.baseUrl : yield* config.baseUrl;
85
+ if (baseUrl !== void 0 && hasQueryOrFragment(baseUrl)) return yield* Effect.die(/* @__PURE__ */ new Error("The baseUrl can't contain a query string or a fragment: pass query parameters with the `query` option of each call"));
86
+ const applyDefaults = makeApplyDefaults(baseUrl, Headers.fromInput(config.headers));
87
+ const chain = applyPlugins(mapClientErrors(provided), plugins, context);
88
+ const httpClient = HttpClient.mapRequestInput(HttpClient.transformResponse(chain, provideOverrides(void 0)), applyDefaults);
89
+ const call = makeCall({
90
+ errorSchema: config.error,
91
+ applyDefaults,
92
+ chain,
93
+ plugins,
94
+ context
95
+ });
96
+ return {
97
+ httpClient,
98
+ request: (method, path, options) => call(method)(path, options),
99
+ get: call("GET"),
100
+ head: call("HEAD"),
101
+ options: call("OPTIONS"),
102
+ post: call("POST"),
103
+ put: call("PUT"),
104
+ patch: call("PATCH"),
105
+ del: call("DELETE")
106
+ };
107
+ });
108
+
109
+ //#endregion
110
+ export { make };
@@ -0,0 +1,224 @@
1
+ import * as Headers from "effect/http/Headers";
2
+ import * as Schema from "effect/Schema";
3
+ //#region src/Errors.d.ts
4
+ declare const NetworkError_base: Schema.Class<
5
+ NetworkError,
6
+ Schema.TaggedStruct<
7
+ "NetworkError",
8
+ {
9
+ readonly method: Schema.String;
10
+ readonly url: Schema.String;
11
+ readonly cause: Schema.Unknown;
12
+ }
13
+ >,
14
+ import("effect/Cause").YieldableError
15
+ >;
16
+ /**
17
+ * The request never produced a response: DNS failure, connection refused,
18
+ * CORS rejection, the device is offline, and so on.
19
+ *
20
+ * `cause` is the underlying error from the platform's HTTP client.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import * as Effect from "effect/Effect";
25
+ * import * as Fetch from "@monarcode/effetch";
26
+ *
27
+ * declare const call: Effect.Effect<string, Fetch.FetchError>;
28
+ *
29
+ * const program = call.pipe(
30
+ * Effect.catchTag("NetworkError", (error) =>
31
+ * Effect.succeed(`Offline? ${error.method} ${error.url}`),
32
+ * ),
33
+ * );
34
+ * ```
35
+ *
36
+ * @since 0.1.0
37
+ */
38
+ export declare class NetworkError extends NetworkError_base {
39
+ /** `"<method> <url>: network error"`. */
40
+ get message(): string;
41
+ }
42
+ declare const HttpError_base: Schema.Class<
43
+ HttpError,
44
+ Schema.TaggedStruct<
45
+ "HttpError",
46
+ {
47
+ readonly method: Schema.String;
48
+ readonly url: Schema.String;
49
+ readonly status: Schema.Number;
50
+ readonly headers: Schema.declare<Headers.Headers, unknown>;
51
+ readonly text: Schema.String;
52
+ readonly body: Schema.Unknown;
53
+ }
54
+ >,
55
+ import("effect/Cause").YieldableError
56
+ >;
57
+ /**
58
+ * The server answered with a non-2xx status.
59
+ *
60
+ * Only 2xx counts as success, as in `fetch`'s `response.ok`. 3xx is an
61
+ * error too: `fetch` follows redirects itself, so a 3xx only reaches you
62
+ * with `redirect: "manual"` or a conditional request answered with
63
+ * `304 Not Modified`. Catch it by `status` if you expect one.
64
+ *
65
+ * - `url` is the request URL without its query string, so secrets in query
66
+ * parameters stay out of logs.
67
+ * - `headers` are the response headers. They're Effect `Headers`, so
68
+ * sensitive ones (`set-cookie`, `authorization`, …) print as
69
+ * `<redacted>` when the error is logged or serialised.
70
+ * - `text` is always the raw response body (`""` if it couldn't be read).
71
+ * - `body` is the body decoded with the call's (or client's) `error` schema,
72
+ * or `undefined` if it didn't match. Without an `error` schema, `body` is
73
+ * the parsed JSON when `text` is valid JSON, else `undefined`. An empty
74
+ * body is always `undefined`. Reading `body` never fails the call.
75
+ *
76
+ * Use {@link HttpError.With} for the type of an `HttpError` whose `body` was
77
+ * decoded with a known schema.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * import * as Effect from "effect/Effect";
82
+ * import * as Fetch from "@monarcode/effetch";
83
+ *
84
+ * declare const call: Effect.Effect<
85
+ * string,
86
+ * Fetch.HttpError.With<{ readonly code: string }>
87
+ * >;
88
+ *
89
+ * const program = call.pipe(
90
+ * Effect.catchTag("HttpError", (error) =>
91
+ * error.status === 404
92
+ * ? Effect.succeed("not found")
93
+ * : Effect.fail(error.body?.code ?? `HTTP ${error.status}`),
94
+ * ),
95
+ * );
96
+ * ```
97
+ *
98
+ * @since 0.1.0
99
+ */
100
+ export declare class HttpError extends HttpError_base {
101
+ /** `"<method> <url>: HTTP <status>"`. */
102
+ get message(): string;
103
+ }
104
+ /**
105
+ * Types related to {@link HttpError}.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import type * as Fetch from "@monarcode/effetch";
110
+ *
111
+ * type NotFound = Fetch.HttpError.With<{ readonly resource: string }>;
112
+ * ```
113
+ *
114
+ * @since 0.1.0
115
+ */
116
+ export declare namespace HttpError {
117
+ /**
118
+ * An {@link HttpError} whose `body` was decoded as `B`. `body` is
119
+ * `undefined` when the response didn't match the error schema.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import type * as Fetch from "@monarcode/effetch";
124
+ *
125
+ * type ApiHttpError = Fetch.HttpError.With<{ readonly code: string }>;
126
+ *
127
+ * const code = (error: ApiHttpError) => error.body?.code;
128
+ * ```
129
+ *
130
+ * @since 0.1.0
131
+ */
132
+ type With<B> = HttpError & {
133
+ readonly body: B | undefined;
134
+ };
135
+ }
136
+ declare const DecodeError_base: Schema.Class<
137
+ DecodeError,
138
+ Schema.TaggedStruct<
139
+ "DecodeError",
140
+ {
141
+ readonly method: Schema.String;
142
+ readonly url: Schema.String;
143
+ readonly status: Schema.Number;
144
+ readonly issue: Schema.Unknown;
145
+ readonly message: Schema.String;
146
+ }
147
+ >,
148
+ import("effect/Cause").YieldableError
149
+ >;
150
+ /**
151
+ * A 2xx response body didn't match the call's `output` schema, wasn't valid
152
+ * JSON, or couldn't be read.
153
+ *
154
+ * `issue` is what went wrong and `message` is a readable summary of it:
155
+ *
156
+ * - the schema issue when the body doesn't match `output`,
157
+ * - the `SyntaxError` when the body isn't valid JSON,
158
+ * - the `HttpClientError` when the body couldn't be read.
159
+ *
160
+ * An empty body (204, HEAD, …) is decoded as JSON `null`, so it only
161
+ * passes schemas that accept nothing, such as `Schema.Void`.
162
+ *
163
+ * `url` is the request URL without its query string.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * import * as Effect from "effect/Effect";
168
+ * import * as Fetch from "@monarcode/effetch";
169
+ *
170
+ * declare const call: Effect.Effect<string, Fetch.FetchError>;
171
+ *
172
+ * const program = call.pipe(
173
+ * Effect.catchTag("DecodeError", (error) =>
174
+ * Effect.succeed(`Unexpected response from ${error.url}: ${error.message}`),
175
+ * ),
176
+ * );
177
+ * ```
178
+ *
179
+ * @since 0.1.0
180
+ */
181
+ export declare class DecodeError extends DecodeError_base {}
182
+ /**
183
+ * Every error the library itself can fail a call with. Plugins may add
184
+ * their own errors on top of these.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * import * as Fetch from "@monarcode/effetch";
189
+ *
190
+ * const describe = (error: Fetch.FetchError): string => {
191
+ * switch (error._tag) {
192
+ * case "NetworkError":
193
+ * return "Network unavailable";
194
+ * case "HttpError":
195
+ * return `Server error ${error.status}`;
196
+ * case "DecodeError":
197
+ * return "Unexpected response";
198
+ * }
199
+ * };
200
+ * ```
201
+ *
202
+ * @since 0.1.0
203
+ */
204
+ export type FetchError = NetworkError | HttpError | DecodeError;
205
+ /**
206
+ * Returns `true` if `u` is a {@link NetworkError}, {@link HttpError} or
207
+ * {@link DecodeError}.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * import * as Fetch from "@monarcode/effetch";
212
+ *
213
+ * const report = (error: unknown) => {
214
+ * if (Fetch.isFetchError(error)) {
215
+ * console.error(error._tag, error.url);
216
+ * }
217
+ * };
218
+ * ```
219
+ *
220
+ * @since 0.1.0
221
+ */
222
+ export declare const isFetchError: (u: unknown) => u is FetchError;
223
+ //#endregion
224
+ export {};