@whatwg-node/server
Version:
Fetch API compliant HTTP Server adapter
226 lines (225 loc) • 8.43 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendNodeResponse = exports.normalizeNodeRequest = exports.isServerResponse = exports.isNodeRequest = exports.isReadable = exports.useNodeAdapter = void 0;
const fetch_1 = require("@whatwg-node/fetch");
const utils_js_1 = require("../utils.js");
function useNodeAdapter() {
const nodeResponseMap = new WeakMap();
return {
onRequestAdapt({ args: [req, res, ...restOfCtx], setRequest, setServerContext, fetchAPI }) {
if (isNodeRequest(req)) {
const defaultServerContext = {
req,
};
const request = normalizeNodeRequest(req, fetchAPI.Request);
setRequest(request);
let ctxParams = restOfCtx;
if (isServerResponse(res)) {
defaultServerContext.res = res;
nodeResponseMap.set(request, res);
}
else {
ctxParams = [res, ...restOfCtx];
}
const serverContext = ctxParams.length > 0 ? (0, utils_js_1.completeAssign)(...ctxParams) : defaultServerContext;
setServerContext(serverContext);
}
},
onResponse({ request, response }) {
const nodeResponse = nodeResponseMap.get(request);
if (nodeResponse) {
return sendNodeResponse(response, nodeResponse);
}
},
};
}
exports.useNodeAdapter = useNodeAdapter;
function isReadable(stream) {
return stream.read != null;
}
exports.isReadable = isReadable;
function isNodeRequest(request) {
return isReadable(request);
}
exports.isNodeRequest = isNodeRequest;
function isServerResponse(stream) {
// Check all used functions are defined
return (stream != null &&
stream.setHeader != null &&
stream.end != null &&
stream.once != null &&
stream.write != null);
}
exports.isServerResponse = isServerResponse;
function getPort(nodeRequest) {
if (nodeRequest.socket?.localPort) {
return nodeRequest.socket?.localPort;
}
const hostInHeader = nodeRequest.headers?.[':authority'] || nodeRequest.headers?.host;
const portInHeader = hostInHeader?.split(':')?.[1];
if (portInHeader) {
return portInHeader;
}
return 80;
}
function getHostnameWithPort(nodeRequest) {
if (nodeRequest.headers?.[':authority']) {
return nodeRequest.headers?.[':authority'];
}
if (nodeRequest.headers?.host) {
return nodeRequest.headers?.host;
}
const port = getPort(nodeRequest);
if (nodeRequest.hostname) {
return nodeRequest.hostname + ':' + port;
}
const localIp = nodeRequest.socket?.localAddress;
if (localIp && !localIp?.includes('::') && !localIp?.includes('ffff')) {
return `${localIp}:${port}`;
}
return 'localhost';
}
function buildFullUrl(nodeRequest) {
const hostnameWithPort = getHostnameWithPort(nodeRequest);
const protocol = nodeRequest.protocol || 'http';
const endpoint = nodeRequest.originalUrl || nodeRequest.url || '/graphql';
return `${protocol}://${hostnameWithPort}${endpoint}`;
}
function isRequestBody(body) {
const stringTag = body[Symbol.toStringTag];
if (typeof body === 'string' ||
stringTag === 'Uint8Array' ||
stringTag === 'Blob' ||
stringTag === 'FormData' ||
stringTag === 'URLSearchParams' ||
(0, utils_js_1.isAsyncIterable)(body)) {
return true;
}
return false;
}
function normalizeNodeRequest(nodeRequest, RequestCtor) {
const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest;
let fullUrl = buildFullUrl(rawRequest);
if (nodeRequest.query) {
const url = new fetch_1.URL(fullUrl);
for (const key in nodeRequest.query) {
url.searchParams.set(key, nodeRequest.query[key]);
}
fullUrl = url.toString();
}
if (nodeRequest.method === 'GET' || nodeRequest.method === 'HEAD') {
return new RequestCtor(fullUrl, {
method: nodeRequest.method,
headers: nodeRequest.headers,
});
}
/**
* Some Node server frameworks like Serverless Express sends a dummy object with body but as a Buffer not string
* so we do those checks to see is there something we can use directly as BodyInit
* because the presence of body means the request stream is already consumed and,
* rawRequest cannot be used as BodyInit/ReadableStream by Fetch API in this case.
*/
const maybeParsedBody = nodeRequest.body;
if (maybeParsedBody != null && Object.keys(maybeParsedBody).length > 0) {
if (isRequestBody(maybeParsedBody)) {
return new RequestCtor(fullUrl, {
method: nodeRequest.method,
headers: nodeRequest.headers,
body: maybeParsedBody,
});
}
const request = new RequestCtor(fullUrl, {
method: nodeRequest.method,
headers: nodeRequest.headers,
});
if (!request.headers.get('content-type')?.includes('json')) {
request.headers.set('content-type', 'application/json; charset=utf-8');
}
return new Proxy(request, {
get: (target, prop, receiver) => {
switch (prop) {
case 'json':
return async () => maybeParsedBody;
case 'text':
return async () => JSON.stringify(maybeParsedBody);
default:
return Reflect.get(target, prop, receiver);
}
},
});
}
// perf: instead of spreading the object, we can just pass it as is and it performs better
return new RequestCtor(fullUrl, {
method: nodeRequest.method,
headers: nodeRequest.headers,
body: rawRequest,
});
}
exports.normalizeNodeRequest = normalizeNodeRequest;
function configureSocket(rawRequest) {
rawRequest?.socket?.setTimeout?.(0);
rawRequest?.socket?.setNoDelay?.(true);
rawRequest?.socket?.setKeepAlive?.(true);
}
function endResponse(serverResponse) {
// @ts-expect-error Avoid arguments adaptor trampoline https://v8.dev/blog/adaptor-frame
serverResponse.end(null, null, null);
}
async function sendAsyncIterable(serverResponse, asyncIterable) {
for await (const chunk of asyncIterable) {
if (!serverResponse
// @ts-expect-error http and http2 writes are actually compatible
.write(chunk)) {
break;
}
}
endResponse(serverResponse);
}
function sendNodeResponse(fetchResponse, serverResponse) {
serverResponse.statusCode = fetchResponse.status;
serverResponse.statusMessage = fetchResponse.statusText;
fetchResponse.headers.forEach((value, key) => {
if (key === 'set-cookie') {
const setCookies = fetchResponse.headers.getSetCookie?.();
if (setCookies) {
serverResponse.setHeader('set-cookie', setCookies);
return;
}
}
serverResponse.setHeader(key, value);
});
// Optimizations for node-fetch
if (fetchResponse.bodyType === 'Buffer' ||
fetchResponse.bodyType === 'String' ||
fetchResponse.bodyType === 'Uint8Array') {
// @ts-expect-error http and http2 writes are actually compatible
serverResponse.write(fetchResponse.bodyInit);
endResponse(serverResponse);
return;
}
// Other fetch implementations
const fetchBody = fetchResponse.body;
if (fetchBody == null) {
endResponse(serverResponse);
return;
}
if (fetchBody[Symbol.toStringTag] === 'Uint8Array') {
serverResponse
// @ts-expect-error http and http2 writes are actually compatible
.write(fetchBody);
endResponse(serverResponse);
return;
}
configureSocket(serverResponse.req);
if (isReadable(fetchBody)) {
serverResponse.once('close', () => {
fetchBody.destroy();
});
fetchBody.pipe(serverResponse);
return;
}
if ((0, utils_js_1.isAsyncIterable)(fetchBody)) {
return sendAsyncIterable(serverResponse, fetchBody);
}
}
exports.sendNodeResponse = sendNodeResponse;