UNPKG

mentoss

Version:

A utility to mock fetch requests and responses.

165 lines (161 loc) 5.38 kB
/** * @fileoverview Various utility functions. * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** * Formats headers into a string that matches how they are displayed in * a network panel. Header names are capitalized and separated by a colon * from the header value. * @param {Headers} headers The headers to format. * @returns {string} The formatted headers. */ function formatHeaders(headers) { return Array.from(headers.entries()) .map(([name, value]) => `${name .split("-") .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join("-")}: ${value}`) .join("\n"); } /** * Formats a body into a string that matches how it is displayed in a network * panel. If the body is an object, it is stringified as JSON. If the body is * a string, it is returned as-is. Otherwise, the body is converted to a string. * @param {string|any|FormData|null} body The body to format. * @returns {string} The formatted body. */ function formatBody(body) { if (!body) { return ""; } if (typeof body === "string") { return body; } if (body.constructor === Object) { return JSON.stringify(body); } // TODO: Other types of bodies return body.toString(); } //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Represents an error that occurs when a URL is invalid. * @extends {Error} */ export class URLParseError extends Error { /** * Creates a new URLParseError instance. * @param {string} url The URL that caused the error. */ constructor(url) { super(`Invalid URL: ${url}`); this.name = "URLParseError"; } } /** * Represents an error that occurs when no route matched a request. * @extends {Error} */ export class NoRouteMatchedError extends Error { /** * Creates a new NoRouteMatchedError instance. * @param {Request} request The request that wasn't matched. * @param {string|any|FormData|null} body The body of the request. * @param {Array<{title: string, messages: Array<string>}>} traces The traces from the servers. */ constructor(request, body, traces) { const message = `No route matched for ${request.method} ${request.url}. Full Request: ${stringifyRequest(request, body)} ${traces.length === 0 ? "No partial matches found." : "Partial matches:\n\n" + traces .map(trace => { let traceMessage = `${trace.title}:`; trace.messages.forEach(message => { traceMessage += `\n ${message}`; }); return traceMessage; }) .join("\n\n")}`; super(message); this.name = "NoRouteMatchedError"; this.request = request; this.body = body; this.traces = traces; } } /** * Parses a URL and returns a URL object. This is used instead * of the URL constructor to provide a standard error message, * because different runtimes use different messages. * @param {string|URL} url The URL to parse. * @returns {URL} The parsed URL. */ export function parseUrl(url) { if (url instanceof URL) { return url; } try { return new URL(url); } catch { throw new URLParseError(url); } } /** * Reads the body from a request based on the HTTP headers. * @param {Request} request The request to read the body from. * @returns {Promise<string|any|FormData|null>} The body of the request. */ export async function getBody(request) { // first get the content type const contentType = request.headers.get("content-type"); // if there's no content type, there's no body if (!contentType) { return Promise.resolve(null); } // next try to read the body as text to see if there's a body const text = await request.clone().text(); // if there's no body, return null if (!text) { return Promise.resolve(null); } // if it's a text format then return a string if (contentType.startsWith("text")) { return text; } // if the content type is JSON, parse the body as JSON if (contentType.startsWith("application/json")) { return request.json(); } // if the content type is form data, parse the body as form data if (contentType.startsWith("multipart/form-data")) { return request.formData(); } // otherwise return the body as bytes return request.arrayBuffer(); } /** * Creates a text representation of a request in the same format as it would * appear in a network panel. * @param {Request} request The request to stringify. * @param {string|any|FormData|null} body The body of the request * @returns {string} The stringified request. */ export function stringifyRequest(request, body) { let text = `${request.method} ${request.url}`; if (request.headers) { text += `\n${formatHeaders(request.headers)}`; } if (body) { text += `\n\n${formatBody(body)}`; } return text.trim(); }