@zimic/fetch
Version:
Next-gen, TypeScript-first fetch-like API client
231 lines (209 loc) • 8.93 kB
text/typescript
import { HttpHeaders, HttpHeadersSchema, HttpSchema, HttpSchemaMethod, HttpSchemaPath } from '@zimic/http';
import { FetchRequest, FetchRequestObject, FetchResponse, FetchResponseObject } from '../types/requests';
/**
* Options for converting a {@link FetchResponseError `FetchResponseError`} into a plain object.
*
* @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponseerrortoobject `FetchResponseError#toObject` API reference}
*/
export interface FetchResponseErrorObjectOptions {
/**
* Whether to include the body of the request in the plain object.
*
* @default false
*/
includeRequestBody?: boolean;
/**
* Whether to include the body of the response in the plain object.
*
* @default false
*/
includeResponseBody?: boolean;
}
export namespace FetchResponseErrorObjectOptions {
/**
* Options for converting a {@link FetchResponseError `FetchResponseError`} into a plain object, including the body of
* the request and/or response.
*/
export type WithBody = FetchResponseErrorObjectOptions &
({ includeRequestBody: true } | { includeResponseBody: true });
/**
* Options for converting a {@link FetchResponseError `FetchResponseError`} into a plain object, excluding the body of
* the request and/or response.
*/
export type WithoutBody = FetchResponseErrorObjectOptions &
({ includeRequestBody?: false } | { includeResponseBody?: false });
}
/**
* A plain object representation of a {@link FetchResponseError `FetchResponseError`}, compatible with JSON. It is useful
* for serialization, debugging, and logging purposes.
*
* @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponseerrortoobject `FetchResponseError#toObject` API reference}
*/
export interface FetchResponseErrorObject {
name: string;
message: string;
request: FetchRequestObject;
response: FetchResponseObject;
}
/**
* An error representing a response with a failure status code (4XX or 5XX).
*
* @example
* import { type HttpSchema } from '@zimic/http';
* import { createFetch } from '@zimic/fetch';
*
* interface User {
* id: string;
* username: string;
* }
*
* type Schema = HttpSchema<{
* '/users/:userId': {
* GET: {
* response: {
* 200: { body: User };
* 404: { body: { message: string } };
* };
* };
* };
* }>;
*
* const fetch = createFetch<Schema>({
* baseURL: 'http://localhost:3000',
* });
*
* const response = await fetch(`/users/${userId}`, {
* method: 'GET',
* });
*
* if (!response.ok) {
* console.log(response.status); // 404
*
* console.log(response.error); // FetchResponseError<Schema, 'GET', '/users'>
* console.log(response.error.request); // FetchRequest<Schema, 'GET', '/users'>
* console.log(response.error.response); // FetchResponse<Schema, 'GET', '/users'>
*
* const plainError = response.error.toObject();
* console.log(JSON.stringify(plainError));
* // {"name":"FetchResponseError","message":"...","request":{...},"response":{...}}
* }
*
* @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponseerror `FetchResponseError` API reference}
*/
class FetchResponseError<
Schema extends HttpSchema,
Method extends HttpSchemaMethod<Schema>,
Path extends HttpSchemaPath.Literal<Schema, Method>,
> extends Error {
constructor(
public request: FetchRequest<Schema, Method, Path>,
public response: FetchResponse<Schema, Method, Path, true, 'manual'>,
) {
super(`${request.method} ${request.url} failed with status ${response.status}: ${response.statusText}`);
this.name = 'FetchResponseError';
}
/**
* Converts this error into a plain object. This method is useful for serialization, debugging, and logging purposes.
*
* @example
* const fetch = createFetch<Schema>({
* baseURL: 'http://localhost:3000',
* });
*
* const response = await fetch(`/users/${userId}`, {
* method: 'GET',
* });
*
* if (!response.ok) {
* const plainError = response.error.toObject();
* console.log(JSON.stringify(plainError));
* // {"name":"FetchResponseError","message":"...","request":{...},"response":{...}}
* }
*
* @param options Options for converting this error into a plain object. By default, the body of the request and
* response will not be included.
* @returns A plain object representing this error. If `options.includeRequestBody` or `options.includeResponseBody`
* is `true`, the body of the request and response will be included, respectively, and the return is a `Promise`.
* Otherwise, the return is the plain object itself without the bodies.
* @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponseerrortoobject `FetchResponseError#toObject` API reference}
*/
toObject(options: FetchResponseErrorObjectOptions.WithBody): Promise<FetchResponseErrorObject>;
toObject(options: FetchResponseErrorObjectOptions.WithoutBody): FetchResponseErrorObject;
toObject(options?: FetchResponseErrorObjectOptions): Promise<FetchResponseErrorObject> | FetchResponseErrorObject;
toObject(options?: FetchResponseErrorObjectOptions): Promise<FetchResponseErrorObject> | FetchResponseErrorObject {
const includeRequestBody = options?.includeRequestBody ?? false;
const includeResponseBody = options?.includeResponseBody ?? false;
const partialObject = {
name: this.name,
message: this.message,
} satisfies Partial<FetchResponseErrorObject>;
if (!includeRequestBody && !includeResponseBody) {
const request = this.convertRequestToObject({ includeBody: false });
const response = this.convertResponseToObject({ includeBody: false });
return { ...partialObject, request, response };
}
return Promise.all([
this.convertRequestToObject({ includeBody: includeRequestBody }),
this.convertResponseToObject({ includeBody: includeResponseBody }),
]).then(([request, response]) => ({ ...partialObject, request, response }));
}
private convertRequestToObject(options: { includeBody: true }): Promise<FetchRequestObject>;
private convertRequestToObject(options: { includeBody: false }): FetchRequestObject;
private convertRequestToObject(options: { includeBody: boolean }): Promise<FetchRequestObject> | FetchRequestObject;
private convertRequestToObject(options: { includeBody: boolean }): Promise<FetchRequestObject> | FetchRequestObject {
const requestObject: FetchRequestObject = {
url: this.request.url,
path: this.request.path,
method: this.request.method,
headers: HttpHeaders.prototype.toObject.call(this.request.headers) as HttpHeadersSchema,
cache: this.request.cache,
destination: this.request.destination,
credentials: this.request.credentials,
integrity: this.request.integrity,
keepalive: this.request.keepalive,
mode: this.request.mode,
redirect: this.request.redirect,
referrer: this.request.referrer,
referrerPolicy: this.request.referrerPolicy,
};
if (!options.includeBody) {
return requestObject;
}
// Optimize type checking by narrowing the type of the body
const bodyAsTextPromise = this.request.text() as Promise<string>;
return bodyAsTextPromise.then((bodyAsText) => {
requestObject.body = bodyAsText.length > 0 ? bodyAsText : null;
return requestObject;
});
}
private convertResponseToObject(options: { includeBody: true }): Promise<FetchResponseObject>;
private convertResponseToObject(options: { includeBody: false }): FetchResponseObject;
private convertResponseToObject(options: {
includeBody: boolean;
}): Promise<FetchResponseObject> | FetchResponseObject;
private convertResponseToObject(options: {
includeBody: boolean;
}): Promise<FetchResponseObject> | FetchResponseObject {
const responseObject: FetchResponseObject = {
url: this.response.url,
type: this.response.type,
status: this.response.status,
statusText: this.response.statusText,
ok: this.response.ok,
headers: HttpHeaders.prototype.toObject.call(this.response.headers) as HttpHeadersSchema,
redirected: this.response.redirected,
};
if (!options.includeBody) {
return responseObject;
}
// Optimize type checking by narrowing the type of the body
const bodyAsTextPromise = this.response.text() as Promise<string>;
return bodyAsTextPromise.then((bodyAsText) => {
responseObject.body = bodyAsText.length > 0 ? bodyAsText : null;
return responseObject;
});
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFetchRequestError = FetchResponseError<any, any, any>;
export default FetchResponseError;