UNPKG

@trpc/server

Version:

The tRPC server library

204 lines (182 loc) • 5.88 kB
import type * as http from 'http'; import { IncomingMessage } from 'node:http'; import { TRPCError } from '../../@trpc/server'; import type { NodeHTTPRequest, NodeHTTPResponse } from './types'; function createBody( req: NodeHTTPRequest, opts: { /** * Max body size in bytes. If the body is larger than this, the request will be aborted */ maxBodySize: number | null; }, ): RequestInit['body'] { let source: NodeHTTPRequest | IncomingMessage = req; // Some adapters will pre-parse the body and add it to the request object if ('body' in req) { if (req.body instanceof IncomingMessage) { // An unparsed stream handed over by the adapter (Fastify does this for // formData), so read it with the size limit source = req.body; } else if (req.body !== undefined) { // If the body is already a string, return it directly if (typeof req.body === 'string') { return req.body; } // If body exists but isn't a string, stringify it as JSON return JSON.stringify(req.body); } else if (req.readableEnded) { // The stream was consumed without producing a parsed body — there is // nothing left to read return undefined; } // The property was defined without the stream being read — e.g. express // 5's body-parser assigns `req.body = undefined` on content types it // skips (such as multipart/form-data) — so stream it like an unparsed // request } else if (req.readableEnded) { // Another middleware drained the stream without handing over what it // read, so waiting for `data`/`end` would hang forever return new ReadableStream({ start(controller) { controller.error( new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'The request body has already been read by another middleware. Assign the parsed body to `req.body` so tRPC can use it', }), ); }, }); } let size = 0; let hasClosed = false; return new ReadableStream({ start(controller) { const onData = (chunk: Buffer) => { size += chunk.length; if (!opts.maxBodySize || size <= opts.maxBodySize) { controller.enqueue( new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), ); return; } controller.error( new TRPCError({ code: 'PAYLOAD_TOO_LARGE', }), ); hasClosed = true; source.off('data', onData); source.off('end', onEnd); }; const onEnd = () => { if (hasClosed) { return; } hasClosed = true; source.off('data', onData); source.off('end', onEnd); controller.close(); }; source.on('data', onData); source.on('end', onEnd); }, cancel() { source.destroy(); }, }); } /** * The full URL of a Node.js request, from its `Host` header and socket. * Throws a `BAD_REQUEST` error if it isn't a valid URL. */ export function createURL(req: NodeHTTPRequest): URL { try { const protocol = // http2 (req.headers[':scheme'] && req.headers[':scheme'] === 'https') || // http1 (req.socket && 'encrypted' in req.socket && req.socket.encrypted) ? 'https:' : 'http:'; const host = req.headers.host ?? req.headers[':authority'] ?? 'localhost'; // oxlint-disable-next-line typescript/no-non-null-assertion return new URL(req.url!, `${protocol}//${host}`); } catch (cause) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid URL', cause, }); } } function createHeaders(incoming: http.IncomingHttpHeaders): Headers { const headers = new Headers(); for (const key in incoming) { const value = incoming[key]; if (typeof key === 'string' && key.startsWith(':')) { // Skip HTTP/2 pseudo-headers continue; } if (Array.isArray(value)) { for (const item of value) { headers.append(key, item); } } else if (value != null) { headers.append(key, value); } } return headers; } /** * Turns a Node.js `IncomingMessage` into a Fetch API `Request`. The * request's signal aborts when the client disconnects. */ export function incomingMessageToRequest( req: NodeHTTPRequest, res: NodeHTTPResponse, opts: { /** * Max body size in bytes. If the body is larger than this, the request will be aborted */ maxBodySize: number | null; }, ): Request { const ac = new AbortController(); const onAbort = () => { res.off('close', onClose); req.off('aborted', onAbort); // abort the request ac.abort(); }; const onClose = () => { // `close` is also emitted after a response that was sent completely, // so only abort if the connection closed before the response was ended if (res.writableEnded) { req.off('aborted', onAbort); return; } onAbort(); }; res.once('close', onClose); req.once('aborted', onAbort); // Get host from either regular header or HTTP/2 pseudo-header const url = createURL(req); const init: RequestInit = { headers: createHeaders(req.headers), method: req.method, signal: ac.signal, }; if (req.method !== 'GET' && req.method !== 'HEAD') { init.body = createBody(req, opts); // init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec. // However, this property is not defined in the TypeScript types for RequestInit, so we have // to cast it here in order to set it without a type error. // See https://fetch.spec.whatwg.org/#dom-requestinit-duplex // @ts-expect-error this is fine init.duplex = 'half'; } const request = new Request(url, init); return request; }