UNPKG

@zimic/http

Version:

Next-gen TypeScript-first HTTP utilities

153 lines (128 loc) 6.13 kB
import HttpFormData from '@/formData/HttpFormData'; import HttpSearchParams from '@/searchParams/HttpSearchParams'; import { HttpBody } from '@/types/requests'; /** * Error thrown when a value is not valid {@link https://developer.mozilla.org/docs/Web/API/FormData FormData}. HTTP * interceptors might throw this error when trying to parse the body of a request or response with the header * `'content-type': 'multipart/form-data'`, if the content cannot be parsed to form data. */ export class InvalidFormDataError extends SyntaxError { constructor(value: string) { super(`Failed to parse value as form data: ${value}`); this.name = 'InvalidFormDataError'; } } /** * Error thrown when a value is not valid JSON. HTTP interceptors might throw this error when trying to parse the body * of a request or response with the header `'content-type': 'application/json'`, if the content cannot be parsed to * JSON. */ export class InvalidJSONError extends SyntaxError { constructor(value: string) { super(`Failed to parse value as JSON: ${value}`); this.name = 'InvalidJSONError'; } } async function parseHttpBodyAsText<Body extends HttpBody>(resource: Request | Response) { const bodyAsText = await resource.text(); return (bodyAsText || null) as Body; } async function parseHttpBodyAsBlob<Body extends HttpBody>(resource: Request | Response) { const bodyAsBlob = await resource.blob(); return bodyAsBlob as Body; } async function parseHttpBodyAsFormData<Body extends HttpBody>(resource: Request | Response) { const resourceClone = resource.clone(); try { const bodyAsRawFormData = await resource.formData(); const bodyAsFormData = new HttpFormData(); for (const [key, value] of bodyAsRawFormData) { bodyAsFormData.append(key, value); } return bodyAsFormData as Body; } catch { const bodyAsText = await resourceClone.text(); if (!bodyAsText.trim()) { return null; } throw new InvalidFormDataError(bodyAsText); } } async function parseHttpBodyAsSearchParams<Body extends HttpBody>(resource: Request | Response) { const bodyAsText = await resource.text(); if (!bodyAsText.trim()) { return null; } const bodyAsSearchParams = new HttpSearchParams(bodyAsText); return bodyAsSearchParams as Body; } async function parseHttpBodyAsJSON<Body extends HttpBody>(resource: Request | Response) { const bodyAsText = await resource.text(); if (!bodyAsText.trim()) { return null; } try { const bodyAsJSON = JSON.parse(bodyAsText) as Body; return bodyAsJSON; } catch { throw new InvalidJSONError(bodyAsText); } } /** * Parses the body of a {@link https://developer.mozilla.org/docs/Web/API/Request request} or * {@link https://developer.mozilla.org/docs/Web/API/Response response} based on its `content-type` header. * * If the body is empty, `null` is returned. If the `content-type` header is not present or not recognized, an attempt * is made to parse the body as JSON, and if that fails, it is returned as a `Blob`. * * | `content-type` | Parsed as | * | ----------------------------------- | ------------------------------------------------------------------------ | * | `application/json` | `JSON` (object) | * | `application/xml` | `string` | * | `application/x-www-form-urlencoded` | [`HttpSearchParams`](https://zimic.dev/docs/http/api/http-search-params) | * | `application/*` (others) | `Blob` | * | `multipart/form-data` | [`HttpFormData`](https://zimic.dev/docs/http/api/http-form-data) | * | `multipart/*` (others) | `Blob` | * | `text/*` | `string` | * | `image/*` | `Blob` | * | `audio/*` | `Blob` | * | `font/*` | `Blob` | * | `video/*` | `Blob` | * | Others | `JSON` if possible, otherwise `Blob` | * * @throws {InvalidJSONError} If the `content-type` starts with `application/json` but the body cannot be parsed to * JSON. * @throws {InvalidFormDataError} If the `content-type` starts with `multipart/form-data` but the body cannot be parsed * to form data. */ export async function parseHttpBody<Body extends HttpBody = HttpBody>(resource: Request | Response) { const contentType = resource.headers.get('content-type'); if (contentType?.startsWith('application/json')) { return parseHttpBodyAsJSON<Body>(resource); } if (contentType?.startsWith('multipart/form-data')) { return parseHttpBodyAsFormData<Body>(resource); } if (contentType?.startsWith('application/x-www-form-urlencoded')) { return parseHttpBodyAsSearchParams<Body>(resource); } if (contentType?.startsWith('text/') || contentType?.startsWith('application/xml')) { return parseHttpBodyAsText<Body>(resource); } if ( contentType?.startsWith('application/') || contentType?.startsWith('image/') || contentType?.startsWith('audio/') || contentType?.startsWith('font/') || contentType?.startsWith('video/') || contentType?.startsWith('multipart/') ) { return parseHttpBodyAsBlob<Body>(resource); } const resourceClone = resource.clone(); try { return await parseHttpBodyAsJSON<Body>(resource); } catch { return parseHttpBodyAsBlob<Body>(resourceClone); } }