import { Buffer } from 'node:buffer'; import { STATUS_CODES } from 'node:http'; import { types } from 'node:util'; import { type RequestInit, request, Headers, FormData as UndiciFormData, Agent } from 'undici'; import type { HeaderRecord } from 'undici/types/header.js'; import type { ResponseLike } from '../shared.js'; export type RequestOptions = Exclude[1], undefined>; let localAgent: Agent | null = null; export async function makeRequest(url: string, init: RequestInit): Promise { // The cast is necessary because `headers` and `method` are narrower types in `undici.request` // our request path guarantees they are of acceptable type for `undici.request` const options = { ...init, body: await resolveBody(init.body), } as RequestOptions; // Mismatched dispatchers from the Node.js-bundled undici and package-installed undici breaks file uploads. // So we ensure that we always pass an Agent to request() // https://github.com/nodejs/node/issues/59012 if (!options.dispatcher) { localAgent ??= new Agent(); options.dispatcher = localAgent; } const res = await request(url, options); return { body: res.body, async arrayBuffer() { return res.body.arrayBuffer(); }, async json() { return res.body.json(); }, async text() { return res.body.text(); }, get bodyUsed() { return res.body.bodyUsed; }, headers: new Headers(res.headers as HeaderRecord), status: res.statusCode, statusText: STATUS_CODES[res.statusCode]!, ok: res.statusCode >= 200 && res.statusCode < 300, }; } export async function resolveBody(body: RequestInit['body']): Promise> { // eslint-disable-next-line no-eq-null, eqeqeq if (body == null) { return null; } else if (typeof body === 'string') { return body; } else if (types.isUint8Array(body)) { return body; } else if (types.isArrayBuffer(body)) { return new Uint8Array(body); } else if (body instanceof URLSearchParams) { return body.toString(); } else if (body instanceof DataView) { return new Uint8Array(body.buffer); } else if (body instanceof Blob) { return new Uint8Array(await body.arrayBuffer()); } else if (body instanceof UndiciFormData) { return body; } else if (body instanceof FormData) { return globalToUndiciFormData(body); } else if ((body as Iterable)[Symbol.iterator]) { const chunks = [...(body as Iterable)]; return Buffer.concat(chunks); } else if ((body as AsyncIterable)[Symbol.asyncIterator]) { const chunks: Uint8Array[] = []; for await (const chunk of body as AsyncIterable) { chunks.push(chunk); } return Buffer.concat(chunks); } throw new TypeError(`Unable to resolve body.`); } function globalToUndiciFormData(fd: globalThis.FormData): UndiciFormData { const clone = new UndiciFormData(); for (const [name, value] of fd.entries()) { if (typeof value === 'string') { clone.append(name, value); } else { clone.append(name, value, value.name); } } return clone; }