mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2025-01-01 03:14:49 -05:00
Made utility methods to work with HTTP responses
This commit is contained in:
parent
af1454f551
commit
456c90f4dc
1 changed files with 414 additions and 0 deletions
414
src/utils/net/http-response.ts
Normal file
414
src/utils/net/http-response.ts
Normal file
|
@ -0,0 +1,414 @@
|
|||
import { ArgumentNullError } from "@/utils/errors";
|
||||
import { asString } from "@/utils/string-utils";
|
||||
import { Headers as NodeFetchHeaders, Response } from "node-fetch";
|
||||
import { Blob } from "./blob";
|
||||
import { FormData } from "./form-data";
|
||||
import { Headers } from "./headers";
|
||||
|
||||
/**
|
||||
* Represents the response to an HTTP request.
|
||||
*/
|
||||
export interface HttpResponse {
|
||||
/**
|
||||
* The response body as a readable stream.
|
||||
*/
|
||||
get body(): NodeJS.ReadableStream | undefined;
|
||||
|
||||
/**
|
||||
* Indicates if the body has been used.
|
||||
*/
|
||||
get bodyUsed(): boolean;
|
||||
|
||||
/**
|
||||
* The headers of the response.
|
||||
*/
|
||||
get headers(): Headers;
|
||||
|
||||
/**
|
||||
* Indicates if the request was successful.
|
||||
*/
|
||||
get ok(): boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the request was redirected.
|
||||
*/
|
||||
get redirected(): boolean;
|
||||
|
||||
/**
|
||||
* The status code of the response.
|
||||
*/
|
||||
get status(): number;
|
||||
|
||||
/**
|
||||
* The status text of the response.
|
||||
*/
|
||||
get statusText(): string;
|
||||
|
||||
/**
|
||||
* The type of the response.
|
||||
*/
|
||||
get type(): HttpResponseType;
|
||||
|
||||
/**
|
||||
* The URL of the response.
|
||||
*/
|
||||
get url(): string;
|
||||
|
||||
/**
|
||||
* Creates a clone of the response object.
|
||||
*/
|
||||
clone(): this;
|
||||
|
||||
/**
|
||||
* Returns the response body as an ArrayBuffer.
|
||||
*/
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
|
||||
/**
|
||||
* Returns the response body as a Blob.
|
||||
*/
|
||||
blob(): Promise<Blob>;
|
||||
|
||||
/**
|
||||
* Returns the response body as FormData.
|
||||
*/
|
||||
formData(): Promise<FormData>;
|
||||
|
||||
/**
|
||||
* Parses the response body as JSON and returns the resulting object.
|
||||
*
|
||||
* @template T - The expected type of the resulting JSON object.
|
||||
*
|
||||
* @returns A Promise that resolves to the parsed JSON object of type `T`.
|
||||
*/
|
||||
json<T>(): Promise<T>;
|
||||
|
||||
/**
|
||||
* Returns the response body as a string.
|
||||
*/
|
||||
text(): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the response to an HTTP request.
|
||||
*/
|
||||
export class HttpResponse {
|
||||
/**
|
||||
* Private constructor to prevent instantiation.
|
||||
*/
|
||||
private constructor() {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cached HTTP response from the given response.
|
||||
*
|
||||
* @param response - The HTTP response to be cached.
|
||||
*
|
||||
* @returns A cached version of the given HTTP response.
|
||||
*/
|
||||
static cache(response: HttpResponse): HttpResponse {
|
||||
return response instanceof CachedHttpResponse ? response : new CachedHttpResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} with a `Blob` body.
|
||||
*
|
||||
* @param blob - The `Blob` instance to be used as the response body.
|
||||
* @param options - Options to configure the response.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
static blob(blob: Blob, options?: HttpResponseOptions): HttpResponse {
|
||||
return HttpResponse.content(blob, "application/octet-stream", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} with a `FormData` body.
|
||||
*
|
||||
* @param formData - The `FormData` instance to be used as the response body.
|
||||
* @param options - Options to configure the response.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
static formData(formData: FormData, options?: HttpResponseOptions): HttpResponse {
|
||||
return HttpResponse.content(formData, "multipart/form-data", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} with a JSON body.
|
||||
*
|
||||
* @param data - The data to be serialized as JSON and used as the response body.
|
||||
* @param options - Options to configure the response.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
static json(data: unknown, options?: HttpResponseOptions): HttpResponse {
|
||||
const serialized = typeof data === "string" ? data : JSON.stringify(data);
|
||||
|
||||
return HttpResponse.content(serialized, "application/json", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} with a text body.
|
||||
*
|
||||
* @param text - The text to be used as the response body.
|
||||
* @param options - Options to configure the response.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
static text(text: string, options?: HttpResponseOptions): HttpResponse {
|
||||
return HttpResponse.content(asString(text), "text/plain", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} with a redirection status.
|
||||
*
|
||||
* @param url - The URL to redirect to.
|
||||
* @param options - Options to configure the response.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
static redirect(url: string | URL, options?: HttpResponseOptions): HttpResponse {
|
||||
return Response.redirect(asString(url), options?.status) as NodeFetchResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} representing an error.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
static error(): HttpResponse {
|
||||
return Response.error() as NodeFetchResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpResponse} with the given content and content type.
|
||||
*
|
||||
* @param data - The data to be used as the response body.
|
||||
* @param contentType - The MIME type of the content.
|
||||
* @param options - Options to configure the response.
|
||||
*
|
||||
* @returns The newly created {@link HttpResponse} instance.
|
||||
*/
|
||||
private static content(data: string | FormData | Blob, contentType: string, options?: HttpResponseOptions): HttpResponse {
|
||||
ArgumentNullError.throwIfNull(data);
|
||||
|
||||
const headers = new NodeFetchHeaders(options?.headers);
|
||||
if (!headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", contentType);
|
||||
}
|
||||
|
||||
return new Response(data, { ...options, headers }) as NodeFetchResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the options for an HTTP response.
|
||||
*/
|
||||
export interface HttpResponseOptions {
|
||||
/**
|
||||
* The headers for the response.
|
||||
*/
|
||||
headers?: Headers;
|
||||
|
||||
/**
|
||||
* The status code for the response.
|
||||
*/
|
||||
status?: number;
|
||||
|
||||
/**
|
||||
* The status text for the response.
|
||||
*/
|
||||
statusText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the type of an HTTP response.
|
||||
*/
|
||||
export type HttpResponseType = "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect";
|
||||
|
||||
/**
|
||||
* Represents a cached version of an HTTP response.
|
||||
*/
|
||||
class CachedHttpResponse implements HttpResponse {
|
||||
/**
|
||||
* The original HttpResponse instance.
|
||||
*/
|
||||
private readonly _response: HttpResponse;
|
||||
|
||||
/**
|
||||
* The cached Blob of the response body.
|
||||
*/
|
||||
private _blob?: Blob;
|
||||
|
||||
/**
|
||||
* The cached FormData of the response body.
|
||||
*/
|
||||
private _formData?: FormData;
|
||||
|
||||
/**
|
||||
* Creates a new {@link CachedHttpResponse} instance.
|
||||
*
|
||||
* @param response - The {@link HttpResponse} to be cached.
|
||||
*/
|
||||
constructor(response: HttpResponse) {
|
||||
this._response = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get body(): NodeJS.ReadableStream {
|
||||
if (this._blob) {
|
||||
return this._blob.stream();
|
||||
}
|
||||
|
||||
if (!this._response.bodyUsed) {
|
||||
return this._response.body;
|
||||
}
|
||||
|
||||
throw new Error("Cannot re-read the response body.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get bodyUsed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get headers(): Headers {
|
||||
return this._response.headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get ok(): boolean {
|
||||
return this._response.ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get redirected(): boolean {
|
||||
return this._response.redirected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get status(): number {
|
||||
return this._response.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get statusText(): string {
|
||||
return this._response.statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get type(): HttpResponseType {
|
||||
return this._response.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get url(): string {
|
||||
return this._response.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
clone(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
const blob = await this.blob();
|
||||
return await blob.arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async blob(): Promise<Blob> {
|
||||
if (this._blob) {
|
||||
return this._blob;
|
||||
}
|
||||
|
||||
if (!this._response.bodyUsed) {
|
||||
this._blob = await this._response.blob();
|
||||
return this._blob;
|
||||
}
|
||||
|
||||
throw new TypeError("Cannot re-read the response as a Blob.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async formData(): Promise<FormData> {
|
||||
if (this._formData) {
|
||||
return this._formData;
|
||||
}
|
||||
|
||||
if (!this._response.bodyUsed) {
|
||||
this._formData = await this._response.formData();
|
||||
return this._formData;
|
||||
}
|
||||
|
||||
throw new TypeError("Cannot re-read the response as a FormData.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async json<T>(): Promise<T> {
|
||||
const text = await this.text();
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
const blob = await this.blob();
|
||||
return await blob.text();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An extension of the `Response` interface from `node-fetch` to fix the non-generic `json(): Promise<unknown>` declaration.
|
||||
*
|
||||
* This interface is used to make it compatible with the `HttpResponse` wrapper while still being able to identify
|
||||
* problems of other mismatches in type definitions between the `node-fetch`'s `Response` and the `HttpResponse` wrapper.
|
||||
*/
|
||||
interface NodeFetchResponse extends Response {
|
||||
/**
|
||||
* Parses the response body as JSON and returns the resulting object.
|
||||
*
|
||||
* @template T - The expected type of the resulting JSON object.
|
||||
*
|
||||
* @returns A Promise that resolves to the parsed JSON object of type `T`.
|
||||
*/
|
||||
json<T>(): Promise<T>;
|
||||
|
||||
/**
|
||||
* Creates a clone of the response object.
|
||||
*/
|
||||
clone(): this;
|
||||
}
|
Loading…
Reference in a new issue