Calling out
The client direction uses toNodeRequestOptions()
to go from a Request to http.request() options and
toWebResponse() to come back. You keep control of the
request itself, which is the point: attach socket event hooks, set timeouts, write
the body your way. If you do not need that control, Node’s global fetch() is simpler.
Buffered
Section titled “Buffered”import http from "node:http";import https from "node:https";import { toNodeRequestOptions, toWebResponse } from "@johnhenry/webwire";
export function nodeFetch(request) { const { isHTTPS, requestOptions } = toNodeRequestOptions(request); const doRequest = isHTTPS ? https.request : http.request; return new Promise((resolve, reject) => { const req = doRequest(requestOptions, async (nodeRes) => { try { const chunks = []; for await (const chunk of nodeRes) chunks.push(chunk); resolve(toWebResponse(nodeRes, Buffer.concat(chunks))); } catch (err) { reject(err); } }); req.on("error", reject); req.end(); });}Choose http or https from isHTTPS: toNodeRequestOptions() does not pick a module
for you, and https.request() is needed for https: URLs.
From a URL, without building a Request
Section titled “From a URL, without building a Request”const { requestOptions } = toNodeRequestOptions("http://localhost:8000/items?id=3", { method: "POST", headers: { "content-type": "application/json" },});const req = http.request(requestOptions, (res) => { /* ... */ });req.end(JSON.stringify({ name: "x" }));options applies only to this bare-URL form. Given a Request, the method and
headers on it win and options is ignored.
Sending a body
Section titled “Sending a body”toNodeRequestOptions() returns no body, even if the Request has one. Write it
yourself:
import { Readable } from "node:stream";
const { requestOptions } = toNodeRequestOptions(request);const req = http.request(requestOptions, onResponse);if (request.body) Readable.fromWeb(request.body).pipe(req);else req.end();When the Request was built with a body, the headers include whatever Content-Type
the constructor derived (a string body gives text/plain;charset=UTF-8), but no
Content-Length. Node will use chunked encoding unless you set one.
Streaming the response
Section titled “Streaming the response”toWebResponse() does not read nodeRes, so a streamed body is just a different
argument. A Node Readable is accepted directly, so you can hand over nodeRes:
const response = toWebResponse(nodeRes, nodeRes); // streams; nothing is bufferedDo this only for statuses that may have a body. In 0.0.0, for 204 and 304 the
Response constructor threw if any body was passed, even an empty one, so branch (from
0.0.1, toWebResponse() does this for 204, 205 and 304 itself, and the
branch is harmless):
const noBody = nodeRes.statusCode === 204 || nodeRes.statusCode === 304;const response = toWebResponse(nodeRes, noBody ? null : nodeRes);A reverse proxy end to end
Section titled “A reverse proxy end to end”toWebRequest(), toNodeRequestOptions(), toWebResponse() and writeWebResponse()
together make a streaming proxy in a page of code:
import http from "node:http";import { Readable } from "node:stream";import { toWebRequest, toNodeRequestOptions, toWebResponse, writeWebResponse,} from "@johnhenry/webwire";
const UPSTREAM = new URL("http://localhost:9000");
http.createServer(async (req, res) => { let request; try { request = toWebRequest(req); } catch (err) { res.statusCode = err.status ?? 500; return res.end(); }
const target = new URL(request.url); target.hostname = UPSTREAM.hostname; target.port = UPSTREAM.port;
const { requestOptions } = toNodeRequestOptions(target, { method: request.method, headers: request.headers, }); requestOptions.headers.host = UPSTREAM.host; // see the note below
const out = http.request(requestOptions, async (nodeRes) => { const noBody = nodeRes.statusCode === 204 || nodeRes.statusCode === 304; await writeWebResponse(toWebResponse(nodeRes, noBody ? null : nodeRes), res); }); out.on("error", () => { if (!res.headersSent) { res.statusCode = 502; res.end(); } });
if (request.body) Readable.fromWeb(request.body).pipe(out); else out.end();}).listen(8000);The body streams through in both directions and repeated Set-Cookie lines survive.
Two things this does not do, and webwire does not do for you:
- Hop-by-hop headers are copied.
connection,keep-aliveandtransfer-encodingfrom the upstream response pass straight through to the client response. Strip the ones you do not want before building theResponse. - The incoming
Hostheader is copied too, so without the override line above the upstream sees the proxy’s host.
For the other direction of mixing in HTTP text formats (turning a captured request into
a Request in the first place), see http-converter.