@fuman/fetch 0.0.3 → 0.0.6

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/_types.d.cts ADDED
@@ -0,0 +1,8 @@
1
+ import { Middleware } from '@fuman/utils';
2
+ import { FfetchAddon } from './addons/types.js';
3
+ export type FetchLike = (req: Request) => Promise<Response>;
4
+ export type FfetchMiddleware = Middleware<Request, Response>;
5
+ export type CombineAddons<ResponseMixins extends FfetchAddon<any, any>[], AccRequest = {}, AccResponse = {}> = ResponseMixins extends [FfetchAddon<infer RequestMixin, infer ResponseMixin>, ...infer Rest extends FfetchAddon<any, any>[]] ? CombineAddons<Rest, AccRequest & RequestMixin, AccResponse & ResponseMixin> : {
6
+ readonly request: AccRequest;
7
+ readonly response: AccResponse;
8
+ };
@@ -0,0 +1,3 @@
1
+ import { FfetchOptions } from '../ffetch.js';
2
+ export declare function urlencode(query: Record<string, unknown>): URLSearchParams;
3
+ export declare function setHeader(options: FfetchOptions, key: string, value: string | null): void;
@@ -0,0 +1,7 @@
1
+ export * from './form.js';
2
+ export * from './multipart.js';
3
+ export * from './parse/addon.js';
4
+ export * from './query.js';
5
+ export * from './rate-limit.js';
6
+ export * from './retry.js';
7
+ export * from './timeout.js';
@@ -0,0 +1,22 @@
1
+ import { FfetchAddon } from './types.js';
2
+ export interface FormAddon {
3
+ /**
4
+ * shorthand for sending form body,
5
+ * mutually exclusive with other body options
6
+ *
7
+ * if form is passed in base options, passing one
8
+ * in the request options will override it completely
9
+ */
10
+ form?: Record<string, unknown>;
11
+ }
12
+ export interface FormAddonOptions {
13
+ /**
14
+ * serializer for the form data.
15
+ * given the form data it should return the serialized data
16
+ *
17
+ * @defaults `URLSearchParams`-based serializer
18
+ * @example `serialize({ a: 123, b: 'hello' }) => 'a=123&b=hello'`
19
+ */
20
+ serialize?: (data: Record<string, unknown>) => BodyInit;
21
+ }
22
+ export declare function form(options?: FormAddonOptions): FfetchAddon<FormAddon, object>;
@@ -0,0 +1,3 @@
1
+ import * as ffetchAddons from './bundle.js';
2
+ export { ffetchAddons };
3
+ export * from './types.js';
@@ -0,0 +1,22 @@
1
+ import { FfetchAddon } from './types.js';
2
+ export interface MultipartAddon {
3
+ /**
4
+ * shorthand for sending multipart form body,
5
+ * useful for file uploads and similar.
6
+ * mutually exclusive with other body options
7
+ *
8
+ * if multipart is passed in base options, passing one
9
+ * in the request options will override it completely
10
+ */
11
+ multipart?: Record<string, unknown>;
12
+ }
13
+ export interface MultipartAddonOptions {
14
+ /**
15
+ * serializer for the form data.
16
+ * given the form data it should return the body
17
+ *
18
+ * @defaults basic `FormData`-based serializer
19
+ */
20
+ serialize?: (data: Record<string, unknown>) => FormData;
21
+ }
22
+ export declare function multipart(options?: MultipartAddonOptions): FfetchAddon<MultipartAddon, object>;
@@ -0,0 +1,11 @@
1
+ export interface FfetchTypeProvider {
2
+ readonly schema: unknown;
3
+ readonly parsed: unknown;
4
+ }
5
+ export interface FfetchParser<TypeProvider extends FfetchTypeProvider> {
6
+ readonly _provider: TypeProvider;
7
+ parse: (schema: unknown, value: unknown) => unknown | Promise<unknown>;
8
+ }
9
+ export type CallTypeProvider<TypeProvider extends FfetchTypeProvider, Schema> = (TypeProvider & {
10
+ schema: Schema;
11
+ })['parsed'];
@@ -0,0 +1,8 @@
1
+ import { BaseIssue, BaseSchema, BaseSchemaAsync, Config, InferOutput } from 'valibot';
2
+ import { FfetchParser, FfetchTypeProvider } from '../_types.js';
3
+ export interface ValibotTypeProvider extends FfetchTypeProvider {
4
+ readonly parsed: this['schema'] extends (BaseSchema<unknown, unknown, BaseIssue<unknown>> | BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>) ? InferOutput<this['schema']> : never;
5
+ }
6
+ export declare function ffetchValibotAdapter({ async, ...rest }?: Partial<Config<BaseIssue<unknown>>> & {
7
+ async?: boolean;
8
+ }): FfetchParser<ValibotTypeProvider>;
@@ -0,0 +1,8 @@
1
+ import { FfetchParser, FfetchTypeProvider } from '../_types.js';
2
+ import type * as v from '@badrap/valita';
3
+ export interface ValitaTypeProvider extends FfetchTypeProvider {
4
+ readonly parsed: this['schema'] extends v.Type<any> ? v.Infer<this['schema']> : never;
5
+ }
6
+ type ParseOptions = NonNullable<Parameters<v.Type<any>['parse']>[1]>;
7
+ export declare function ffetchValitaAdapter(options?: ParseOptions): FfetchParser<ValitaTypeProvider>;
8
+ export {};
@@ -0,0 +1,8 @@
1
+ import { FfetchParser, FfetchTypeProvider } from '../_types.js';
2
+ import type * as v from '@badrap/valita';
3
+ export interface ValitaTypeProvider extends FfetchTypeProvider {
4
+ readonly parsed: this['schema'] extends v.Type<any> ? v.Infer<this['schema']> : never;
5
+ }
6
+ type ParseOptions = NonNullable<Parameters<v.Type<any>['parse']>[1]>;
7
+ export declare function ffetchValitaAdapter(options?: ParseOptions): FfetchParser<ValitaTypeProvider>;
8
+ export {};
@@ -0,0 +1,13 @@
1
+ import { CastOptions, InferType, ISchema, ValidateOptions } from 'yup';
2
+ import { FfetchParser, FfetchTypeProvider } from '../_types.js';
3
+ export interface YupTypeProvider extends FfetchTypeProvider {
4
+ readonly parsed: this['schema'] extends ISchema<any, any> ? InferType<this['schema']> : never;
5
+ }
6
+ export type FfetchYupAdapterOptions = {
7
+ action: 'cast';
8
+ options?: CastOptions;
9
+ } | {
10
+ action: 'validate';
11
+ options?: ValidateOptions;
12
+ };
13
+ export declare function ffetchYupAdapter({ action, options, }?: FfetchYupAdapterOptions): FfetchParser<YupTypeProvider>;
@@ -0,0 +1,6 @@
1
+ import { ParseParams, z } from 'zod';
2
+ import { FfetchParser, FfetchTypeProvider } from '../_types.js';
3
+ export interface ZodTypeProvider extends FfetchTypeProvider {
4
+ readonly parsed: this['schema'] extends z.ZodTypeAny ? z.infer<this['schema']> : never;
5
+ }
6
+ export declare function ffetchZodAdapter({ async, ...rest }?: Partial<ParseParams>): FfetchParser<ZodTypeProvider>;
@@ -0,0 +1,6 @@
1
+ import { FfetchAddon } from '../types.js';
2
+ import { CallTypeProvider, FfetchParser, FfetchTypeProvider } from './_types.js';
3
+ export { FfetchParser, FfetchTypeProvider };
4
+ export declare function parser<TypeProvider extends FfetchTypeProvider>(parser: FfetchParser<TypeProvider>): FfetchAddon<object, {
5
+ parsedJson: <Schema>(schema: Schema) => Promise<CallTypeProvider<TypeProvider, Schema>>;
6
+ }>;
@@ -0,0 +1,17 @@
1
+ import { FfetchAddon } from './types.js';
2
+ export interface QueryAddon {
3
+ /** query params to be appended to the url */
4
+ query?: Record<string, unknown>;
5
+ }
6
+ export interface QueryAddonOptions {
7
+ /**
8
+ * serializer for the query params.
9
+ * given the query params and the url, it should return the serialized url
10
+ * with the query params added
11
+ *
12
+ * @defaults `URLSearchParams`-based serializer, preserving all existing query params
13
+ * @example `serialize({ a: 123, b: 'hello' }, 'https://example.com/api') => 'https://example.com/api?a=123&b=hello'`
14
+ */
15
+ serialize?: (query: Record<string, unknown>, url: string) => string;
16
+ }
17
+ export declare function query(options?: QueryAddonOptions): FfetchAddon<QueryAddon, object>;
@@ -0,0 +1,62 @@
1
+ import { FfetchAddon } from './types.js';
2
+ import { MaybePromise } from '@fuman/utils';
3
+ export interface RateLimitAddon {
4
+ rateLimit?: {
5
+ /**
6
+ * check if the request was rejected due to rate limit
7
+ *
8
+ * @default `res => res.status === 429`
9
+ */
10
+ isRejected?: (res: Response) => MaybePromise<boolean>;
11
+ /**
12
+ * getter for the unix timestamp of the next reset
13
+ * can either be a unix timestamp in seconds or an ISO 8601 date string
14
+ *
15
+ * @default `res => res.headers.get('x-ratelimit-reset')`
16
+ */
17
+ getReset?: (res: Response) => MaybePromise<string | number | null>;
18
+ /**
19
+ * when the rate limit is exceeded (i.e. `isRejected` returns true),
20
+ * but the reset time is unknown (i.e. `getReset` returns `null`),
21
+ * what is the default time to wait until the rate limit is reset?
22
+ * in milliseconds
23
+ *
24
+ * @default `30_000`
25
+ */
26
+ defaultWaitTime?: number;
27
+ /**
28
+ * number of milliseconds to add to the reset time when the rate limit is exceeded,
29
+ * to account for network latency and other factors
30
+ *
31
+ * @default `5000`
32
+ */
33
+ jitter?: number;
34
+ /**
35
+ * when the rate limit has exceeded (i.e. `isRejected` returns true),
36
+ * what is the maximum acceptable time to wait until the rate limit is reset?
37
+ * in milliseconds
38
+ *
39
+ * @default `300_000`
40
+ */
41
+ maxWaitTime?: number;
42
+ /**
43
+ * maximum number of retries
44
+ *
45
+ * @default `3`
46
+ */
47
+ maxRetries?: number;
48
+ /**
49
+ * function that will be called when the rate limit is exceeded (i.e. `isRejected` returns true),
50
+ * but before starting the wait timer
51
+ *
52
+ * @param res the response that caused the rate limit to be exceeded
53
+ * @param waitTime the time to wait until the rate limit is reset (in milliseconds)
54
+ */
55
+ onRateLimitExceeded?: (res: Response, waitTime: number) => void;
56
+ };
57
+ }
58
+ /**
59
+ * ffetch addon that handles "rate limit exceeded" errors,
60
+ * and waits until the rate limit is reset
61
+ */
62
+ export declare function rateLimitHandler(): FfetchAddon<RateLimitAddon, object>;
@@ -0,0 +1,59 @@
1
+ import { FfetchAddon } from './types.js';
2
+ export declare class RetriesExceededError extends Error {
3
+ readonly retries: number;
4
+ readonly request: Request;
5
+ constructor(retries: number, request: Request);
6
+ }
7
+ export interface RetryOptions {
8
+ /**
9
+ * max number of retries
10
+ * @default 5
11
+ */
12
+ maxRetries?: number;
13
+ /**
14
+ * delay between retries
15
+ * @default retryCount * 1000, up to 5000
16
+ */
17
+ retryDelay?: number | ((retryCount: number) => number);
18
+ /**
19
+ * function that will be called before starting the retry loop.
20
+ * if it returns false, the retry loop will be skipped and
21
+ * the error will be thrown immediately
22
+ *
23
+ * @default () => false
24
+ */
25
+ skip?: (request: Request) => boolean;
26
+ /**
27
+ * function that will be called before a retry is attempted,
28
+ * and can be used to modify the request before proceeding
29
+ *
30
+ * @param attempt current retry attempt (starts at 0)
31
+ */
32
+ onRetry?: (attempt: number, request: Request) => Request | void;
33
+ /**
34
+ * function that will be called whenever a response is received,
35
+ * and should return whether the response is valid (i.e. should be returned and not retried)
36
+ *
37
+ * @default `response => response.status < 500`
38
+ */
39
+ onResponse?: (response: Response, request: Request) => boolean;
40
+ /**
41
+ * function that will be called if an error is thrown while calling
42
+ * the rest of the middleware chain,
43
+ * and should return whether the error should be retried
44
+ *
45
+ * @default `() => true`
46
+ */
47
+ onError?: (err: unknown, request: Request) => boolean;
48
+ /**
49
+ * if true, the last response will be returned if the number of retries is exceeded
50
+ * instead of throwing {@link RetriesExceededError}
51
+ *
52
+ * @default false
53
+ */
54
+ returnLastResponse?: boolean;
55
+ }
56
+ export interface RetryAddon {
57
+ retry?: RetryOptions | false;
58
+ }
59
+ export declare function retry(): FfetchAddon<RetryAddon, object>;
package/addons/retry.d.ts CHANGED
@@ -53,6 +53,7 @@ export interface RetryOptions {
53
53
  */
54
54
  returnLastResponse?: boolean;
55
55
  }
56
- export declare function retry(): FfetchAddon<{
56
+ export interface RetryAddon {
57
57
  retry?: RetryOptions | false;
58
- }, object>;
58
+ }
59
+ export declare function retry(): FfetchAddon<RetryAddon, object>;
@@ -0,0 +1,25 @@
1
+ import { FetchAddonCtx, FfetchAddon } from './types.js';
2
+ export declare class TimeoutError extends Error {
3
+ readonly timeout: number;
4
+ constructor(timeout: number);
5
+ }
6
+ export interface TimeoutAddon {
7
+ /**
8
+ * timeout for the request in ms
9
+ *
10
+ * pass `Infinity` or `0` to disable the default timeout from the base options
11
+ *
12
+ * when the timeout is reached, the request will be aborted
13
+ * and the promise will be rejected with a TimeoutError
14
+ */
15
+ timeout?: number | ((ctx: FetchAddonCtx<TimeoutAddon>) => number);
16
+ }
17
+ /**
18
+ * ffetch addon that allows setting a timeout for the request.
19
+ * when the timeout is reached, the request will be aborted
20
+ * and the promise will be rejected with a TimeoutError
21
+ *
22
+ * **note**: it is important to put this addon as the last one,
23
+ * otherwise other middlewares might be counted towards the timeout
24
+ */
25
+ export declare function timeout(): FfetchAddon<TimeoutAddon, object>;
@@ -0,0 +1,7 @@
1
+ import { CookieJar } from 'tough-cookie';
2
+ import { FfetchAddon } from './types.js';
3
+ export interface FfetchToughCookieAddon {
4
+ /** cookie jar to use */
5
+ cookies?: CookieJar;
6
+ }
7
+ export declare function toughCookieAddon(): FfetchAddon<FfetchToughCookieAddon, object>;
@@ -0,0 +1,30 @@
1
+ import { FfetchOptions, FfetchResult } from '../ffetch.js';
2
+ /**
3
+ * context that is passed to each addon in the order they were added
4
+ * you can safely modify anything in this object
5
+ */
6
+ export interface FetchAddonCtx<RequestMixin extends object> {
7
+ /** url of the request (with baseUrl already applied) */
8
+ url: string;
9
+ /** options of this specific request */
10
+ options: FfetchOptions & RequestMixin;
11
+ /** base options passed to `createFfetch` */
12
+ baseOptions: FfetchOptions & RequestMixin;
13
+ }
14
+ /** internals that are exposed to the functions in response mixin */
15
+ export type FfetchResultInternals<RequestMixin extends object> = FfetchResult & {
16
+ /** final url of the request */
17
+ _url: string;
18
+ /** request init object that will be passed to fetch */
19
+ _init: RequestInit;
20
+ /** finalized and merged options */
21
+ _options: FfetchOptions & RequestMixin;
22
+ /** finalized and merged headers */
23
+ _headers?: Record<string, string>;
24
+ };
25
+ export interface FfetchAddon<RequestMixin extends object, ResponseMixin extends object> {
26
+ /** function that will be called before each request */
27
+ beforeRequest?: (ctx: FetchAddonCtx<RequestMixin>) => void;
28
+ /** mixin functions that will be added to the response promise */
29
+ response?: ResponseMixin;
30
+ }
package/default.cjs CHANGED
@@ -5,11 +5,13 @@ const timeout = require("./addons/timeout.cjs");
5
5
  const query = require("./addons/query.cjs");
6
6
  const form = require("./addons/form.cjs");
7
7
  const multipart = require("./addons/multipart.cjs");
8
+ const retry = require("./addons/retry.cjs");
8
9
  const ffetchDefaultAddons = [
9
10
  /* @__PURE__ */ timeout.timeout(),
10
11
  /* @__PURE__ */ query.query(),
11
12
  /* @__PURE__ */ form.form(),
12
- /* @__PURE__ */ multipart.multipart()
13
+ /* @__PURE__ */ multipart.multipart(),
14
+ /* @__PURE__ */ retry.retry()
13
15
  ];
14
16
  const ffetchBase = /* @__PURE__ */ ffetch.createFfetch({
15
17
  addons: ffetchDefaultAddons
package/default.d.cts ADDED
@@ -0,0 +1,31 @@
1
+ import { FfetchAddon, ffetchAddons } from './addons/index.js';
2
+ import { Ffetch } from './ffetch.js';
3
+ export declare const ffetchDefaultAddons: [
4
+ FfetchAddon<ffetchAddons.TimeoutAddon, object>,
5
+ FfetchAddon<ffetchAddons.QueryAddon, object>,
6
+ FfetchAddon<ffetchAddons.FormAddon, object>,
7
+ FfetchAddon<ffetchAddons.MultipartAddon, object>,
8
+ FfetchAddon<ffetchAddons.RetryAddon, object>
9
+ ];
10
+ /**
11
+ * the default ffetch instance with a reasonable default set of addons
12
+ *
13
+ * you can use this as a base to create your project-specific fetch instance,
14
+ * or use this as is.
15
+ *
16
+ * this is not exported as `ffetch` because most of the time you will want to extend it,
17
+ * and exporting it as `ffetch` would make them clash in import suggestions,
18
+ * and will also make it prone to subtle bugs.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { ffetchBase } from '@fuman/fetch'
23
+ *
24
+ * const ffetch = ffetchBase.extend({
25
+ * baseUrl: 'https://example.com',
26
+ * headers: { ... },
27
+ * addons: [ ... ],
28
+ * })
29
+ * ```
30
+ */
31
+ export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon & ffetchAddons.RetryAddon, object>;
package/default.d.ts CHANGED
@@ -4,10 +4,11 @@ export declare const ffetchDefaultAddons: [
4
4
  FfetchAddon<ffetchAddons.TimeoutAddon, object>,
5
5
  FfetchAddon<ffetchAddons.QueryAddon, object>,
6
6
  FfetchAddon<ffetchAddons.FormAddon, object>,
7
- FfetchAddon<ffetchAddons.MultipartAddon, object>
7
+ FfetchAddon<ffetchAddons.MultipartAddon, object>,
8
+ FfetchAddon<ffetchAddons.RetryAddon, object>
8
9
  ];
9
10
  /**
10
- * the default ffetch instance with reasonable default set of addons
11
+ * the default ffetch instance with a reasonable default set of addons
11
12
  *
12
13
  * you can use this as a base to create your project-specific fetch instance,
13
14
  * or use this as is.
@@ -27,4 +28,4 @@ export declare const ffetchDefaultAddons: [
27
28
  * })
28
29
  * ```
29
30
  */
30
- export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon, object>;
31
+ export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon & ffetchAddons.RetryAddon, object>;
package/default.js CHANGED
@@ -3,11 +3,13 @@ import { timeout } from "./addons/timeout.js";
3
3
  import { query } from "./addons/query.js";
4
4
  import { form } from "./addons/form.js";
5
5
  import { multipart } from "./addons/multipart.js";
6
+ import { retry } from "./addons/retry.js";
6
7
  const ffetchDefaultAddons = [
7
8
  /* @__PURE__ */ timeout(),
8
9
  /* @__PURE__ */ query(),
9
10
  /* @__PURE__ */ form(),
10
- /* @__PURE__ */ multipart()
11
+ /* @__PURE__ */ multipart(),
12
+ /* @__PURE__ */ retry()
11
13
  ];
12
14
  const ffetchBase = /* @__PURE__ */ createFfetch({
13
15
  addons: ffetchDefaultAddons
package/ffetch.cjs CHANGED
@@ -7,6 +7,8 @@ class HttpError extends Error {
7
7
  super(`HTTP Error ${response.status} ${response.statusText}`);
8
8
  this.response = response;
9
9
  }
10
+ body = null;
11
+ bodyText = null;
10
12
  }
11
13
  function headersToObject(headers) {
12
14
  if (!headers) return {};
@@ -45,15 +47,27 @@ class FfetchResultImpl {
45
47
  }
46
48
  async #fetchAndValidate() {
47
49
  const res = await this.#fetch(new Request(this._url, this._init));
50
+ let err = null;
48
51
  if (this._options.validateResponse === void 0 || this._options.validateResponse !== false) {
49
52
  if (typeof this._options.validateResponse === "function") {
50
53
  if (!await this._options.validateResponse(res)) {
51
- throw new HttpError(res);
54
+ err = new HttpError(res);
52
55
  }
53
56
  } else if (!res.ok) {
54
- throw new HttpError(res);
57
+ err = new HttpError(res);
55
58
  }
56
59
  }
60
+ if (err != null) {
61
+ if (this._options.readBodyOnError !== false) {
62
+ try {
63
+ ;
64
+ err.body = new Uint8Array(await res.arrayBuffer());
65
+ err.bodyText = utils.utf8.decoder.decode(err.body);
66
+ } catch {
67
+ }
68
+ }
69
+ throw err;
70
+ }
57
71
  return res;
58
72
  }
59
73
  async raw() {
@@ -142,8 +156,16 @@ function createFfetch(baseOptions = {}) {
142
156
  if (options.middlewares !== void 0 && options.middlewares.length > 0) {
143
157
  fetcher = utils.composeMiddlewares(options.middlewares, wrappedFetch);
144
158
  }
145
- if (baseOptions?.baseUrl != null || options.baseUrl != null) {
146
- url = new URL(url, options.baseUrl ?? baseOptions?.baseUrl).href;
159
+ if ((baseOptions?.baseUrl != null || options.baseUrl != null) && !url.includes("://")) {
160
+ const baseUrl = options.baseUrl ?? baseOptions?.baseUrl;
161
+ let prepend = baseUrl;
162
+ if (prepend[prepend.length - 1] !== "/") {
163
+ prepend += "/";
164
+ }
165
+ if (url[0] === "/") {
166
+ url = url.slice(1);
167
+ }
168
+ url = prepend + url;
147
169
  }
148
170
  let init;
149
171
  let headers;
package/ffetch.d.cts ADDED
@@ -0,0 +1,115 @@
1
+ import { CombineAddons, FfetchMiddleware } from './_types.js';
2
+ import { FfetchAddon } from './addons/types.js';
3
+ export interface FfetchOptions {
4
+ /**
5
+ * http method
6
+ * @default 'GET'
7
+ */
8
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | (string & {});
9
+ /**
10
+ * whether to throw HttpError on non-2xx responses
11
+ *
12
+ * when a function is provided, it will be called with the response
13
+ * and should return whether the response is valid.
14
+ * if it returns false, the response will be thrown as an HttpError
15
+ *
16
+ * @default true
17
+ */
18
+ validateResponse?: false | ((res: Response) => boolean | Promise<boolean>);
19
+ /**
20
+ * whether to read the body of the response on HttpError (i.e. when `validateResponse` returns false).
21
+ * useful for debugging, but may be undesirable in some cases.
22
+ *
23
+ * @default true
24
+ */
25
+ readBodyOnError?: boolean;
26
+ /**
27
+ * base url to be prepended to the url
28
+ *
29
+ * this base url is **always** treated as a "base path", i.e. the path passed to `ffetch()`
30
+ * will always be appended to it (unlike the `new URL()`, which has ambiguous slash semantics)
31
+ */
32
+ baseUrl?: string;
33
+ /** body to be passed to fetch() */
34
+ body?: BodyInit;
35
+ /**
36
+ * shorthand for sending json body.
37
+ * mutually exclusive with `body`
38
+ */
39
+ json?: unknown;
40
+ /** headers to be passed to fetch() */
41
+ headers?: HeadersInit;
42
+ /** middlewares for the requests */
43
+ middlewares?: FfetchMiddleware[];
44
+ /** any additional options to be passed to fetch() */
45
+ extra?: RequestInit;
46
+ }
47
+ export interface FfetchBaseOptions<Addons extends FfetchAddon<any, any>[] = FfetchAddon<any, any>[]> extends FfetchOptions {
48
+ /** implementation of fetch() */
49
+ fetch?: typeof fetch;
50
+ /** addons for the request */
51
+ addons?: Addons;
52
+ /**
53
+ * whether to capture stack trace for errors
54
+ * may slightly impact performance
55
+ *
56
+ * @default true
57
+ */
58
+ captureStackTrace?: boolean;
59
+ }
60
+ export interface FfetchResult extends Promise<Response> {
61
+ raw: () => Promise<Response>;
62
+ stream: () => Promise<ReadableStream<Uint8Array>>;
63
+ json: <T = unknown>() => Promise<T>;
64
+ text: () => Promise<string>;
65
+ arrayBuffer: () => Promise<ArrayBuffer>;
66
+ blob: () => Promise<Blob>;
67
+ }
68
+ /**
69
+ * the main function of the library
70
+ *
71
+ * @param url url to fetch (or path, if baseUrl is set)
72
+ * @param params options (note that the function may mutate the object, do not rely on its immutability)
73
+ */
74
+ export interface Ffetch<RequestMixin, ResponseMixin> {
75
+ (url: string, params?: FfetchOptions & RequestMixin): FfetchResult & ResponseMixin;
76
+ /** shorthand for making a GET request */
77
+ get: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
78
+ /** shorthand for making a POST request */
79
+ post: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
80
+ /** shorthand for making a PUT request */
81
+ put: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
82
+ /** shorthand for making a DELETE request */
83
+ delete: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
84
+ /** shorthand for making a PATCH request */
85
+ patch: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
86
+ /** shorthand for making a HEAD request */
87
+ head: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
88
+ /** shorthand for making an OPTIONS request */
89
+ options: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
90
+ /**
91
+ * extend the base options with the given options
92
+ *
93
+ * note: addons, middlewares and headers will be merged with the base options,
94
+ * the rest of the options will be overridden
95
+ */
96
+ extend: <const Addons extends FfetchAddon<any, any>[], Combined extends {
97
+ request: object;
98
+ response: object;
99
+ } = CombineAddons<Addons>>(baseOptions: FfetchBaseOptions<Addons> & Combined['request']) => Ffetch<RequestMixin & Combined['request'], ResponseMixin & Combined['response']>;
100
+ }
101
+ /**
102
+ * an error that is thrown when the response status is not 2xx,
103
+ * or `validateResponse` returns false
104
+ */
105
+ export declare class HttpError extends Error {
106
+ readonly response: Response;
107
+ readonly body: Uint8Array | null;
108
+ readonly bodyText: string | null;
109
+ constructor(response: Response);
110
+ }
111
+ /** create a new ffetch function with the given base options */
112
+ export declare function createFfetch<const Addons extends FfetchAddon<any, any>[], Combined extends {
113
+ request: object;
114
+ response: object;
115
+ } = CombineAddons<Addons>>(baseOptions?: FfetchBaseOptions<Addons> & Combined['request']): Ffetch<Combined['request'], Combined['response']>;
package/ffetch.d.ts CHANGED
@@ -16,7 +16,19 @@ export interface FfetchOptions {
16
16
  * @default true
17
17
  */
18
18
  validateResponse?: false | ((res: Response) => boolean | Promise<boolean>);
19
- /** base url to be prepended to the url */
19
+ /**
20
+ * whether to read the body of the response on HttpError (i.e. when `validateResponse` returns false).
21
+ * useful for debugging, but may be undesirable in some cases.
22
+ *
23
+ * @default true
24
+ */
25
+ readBodyOnError?: boolean;
26
+ /**
27
+ * base url to be prepended to the url
28
+ *
29
+ * this base url is **always** treated as a "base path", i.e. the path passed to `ffetch()`
30
+ * will always be appended to it (unlike the `new URL()`, which has ambiguous slash semantics)
31
+ */
20
32
  baseUrl?: string;
21
33
  /** body to be passed to fetch() */
22
34
  body?: BodyInit;
@@ -92,6 +104,8 @@ export interface Ffetch<RequestMixin, ResponseMixin> {
92
104
  */
93
105
  export declare class HttpError extends Error {
94
106
  readonly response: Response;
107
+ readonly body: Uint8Array | null;
108
+ readonly bodyText: string | null;
95
109
  constructor(response: Response);
96
110
  }
97
111
  /** create a new ffetch function with the given base options */
package/ffetch.js CHANGED
@@ -1,10 +1,12 @@
1
- import { composeMiddlewares, unknownToError } from "@fuman/utils";
1
+ import { composeMiddlewares, utf8, unknownToError } from "@fuman/utils";
2
2
  const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
3
3
  class HttpError extends Error {
4
4
  constructor(response) {
5
5
  super(`HTTP Error ${response.status} ${response.statusText}`);
6
6
  this.response = response;
7
7
  }
8
+ body = null;
9
+ bodyText = null;
8
10
  }
9
11
  function headersToObject(headers) {
10
12
  if (!headers) return {};
@@ -43,15 +45,27 @@ class FfetchResultImpl {
43
45
  }
44
46
  async #fetchAndValidate() {
45
47
  const res = await this.#fetch(new Request(this._url, this._init));
48
+ let err = null;
46
49
  if (this._options.validateResponse === void 0 || this._options.validateResponse !== false) {
47
50
  if (typeof this._options.validateResponse === "function") {
48
51
  if (!await this._options.validateResponse(res)) {
49
- throw new HttpError(res);
52
+ err = new HttpError(res);
50
53
  }
51
54
  } else if (!res.ok) {
52
- throw new HttpError(res);
55
+ err = new HttpError(res);
53
56
  }
54
57
  }
58
+ if (err != null) {
59
+ if (this._options.readBodyOnError !== false) {
60
+ try {
61
+ ;
62
+ err.body = new Uint8Array(await res.arrayBuffer());
63
+ err.bodyText = utf8.decoder.decode(err.body);
64
+ } catch {
65
+ }
66
+ }
67
+ throw err;
68
+ }
55
69
  return res;
56
70
  }
57
71
  async raw() {
@@ -140,8 +154,16 @@ function createFfetch(baseOptions = {}) {
140
154
  if (options.middlewares !== void 0 && options.middlewares.length > 0) {
141
155
  fetcher = composeMiddlewares(options.middlewares, wrappedFetch);
142
156
  }
143
- if (baseOptions?.baseUrl != null || options.baseUrl != null) {
144
- url = new URL(url, options.baseUrl ?? baseOptions?.baseUrl).href;
157
+ if ((baseOptions?.baseUrl != null || options.baseUrl != null) && !url.includes("://")) {
158
+ const baseUrl = options.baseUrl ?? baseOptions?.baseUrl;
159
+ let prepend = baseUrl;
160
+ if (prepend[prepend.length - 1] !== "/") {
161
+ prepend += "/";
162
+ }
163
+ if (url[0] === "/") {
164
+ url = url.slice(1);
165
+ }
166
+ url = prepend + url;
145
167
  }
146
168
  let init;
147
169
  let headers;
package/index.d.cts ADDED
@@ -0,0 +1,4 @@
1
+ export type { FfetchMiddleware } from './_types.js';
2
+ export * from './addons/index.js';
3
+ export * from './default.js';
4
+ export * from './ffetch.js';
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@fuman/fetch",
3
3
  "type": "module",
4
- "version": "0.0.3",
4
+ "version": "0.0.6",
5
5
  "description": "tiny wrapper over fetch",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
- "@fuman/utils": "^0.0.3"
8
+ "@fuman/utils": "^0.0.4"
9
9
  },
10
10
  "peerDependencies": {
11
+ "@badrap/valita": ">=0.4.0",
11
12
  "tough-cookie": "^5.0.0 || ^4.0.0",
12
13
  "valibot": "^0.42.0",
13
14
  "yup": "^1.0.0",
@@ -54,6 +55,16 @@
54
55
  "default": "./yup.cjs"
55
56
  }
56
57
  },
58
+ "./valita": {
59
+ "import": {
60
+ "types": "./valita.d.ts",
61
+ "default": "./valita.js"
62
+ },
63
+ "require": {
64
+ "types": "./valita.d.cts",
65
+ "default": "./valita.cjs"
66
+ }
67
+ },
57
68
  "./tough": {
58
69
  "import": {
59
70
  "types": "./tough.d.ts",
@@ -67,6 +78,9 @@
67
78
  },
68
79
  "sideEffects": false,
69
80
  "peerDependenciesMeta": {
81
+ "@badrap/valita": {
82
+ "optional": true
83
+ },
70
84
  "tough-cookie": {
71
85
  "optional": true
72
86
  },
package/tough.d.cts ADDED
@@ -0,0 +1 @@
1
+ export * from "./addons/tough-cookie.js"
package/valibot.d.cts ADDED
@@ -0,0 +1 @@
1
+ export * from "./addons/parse/adapters/valibot.js"
package/valita.cjs ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function ffetchValitaAdapter(options) {
4
+ const _provider = null;
5
+ return {
6
+ _provider,
7
+ parse(schema, value) {
8
+ return schema.parse(value, options);
9
+ }
10
+ };
11
+ }
12
+ exports.ffetchValitaAdapter = ffetchValitaAdapter;
package/valita.d.cts ADDED
@@ -0,0 +1 @@
1
+ export * from "./addons/parse/adapters/valita.js"
package/valita.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./addons/parse/adapters/valita.js"
package/valita.js ADDED
@@ -0,0 +1,12 @@
1
+ function ffetchValitaAdapter(options) {
2
+ const _provider = null;
3
+ return {
4
+ _provider,
5
+ parse(schema, value) {
6
+ return schema.parse(value, options);
7
+ }
8
+ };
9
+ }
10
+ export {
11
+ ffetchValitaAdapter
12
+ };
package/yup.d.cts ADDED
@@ -0,0 +1 @@
1
+ export * from "./addons/parse/adapters/yup.js"
package/zod.d.cts ADDED
@@ -0,0 +1 @@
1
+ export * from "./addons/parse/adapters/zod.js"