@whatwg-node/server
Version:
Fetch API compliant HTTP Server adapter
513 lines (505 loc) • 20.1 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const fetch = require('@whatwg-node/fetch');
function isAsyncIterable(body) {
return body != null && typeof body === 'object' && typeof body[Symbol.asyncIterator] === 'function';
}
function getPort(nodeRequest) {
var _a, _b, _c, _d, _e;
if ((_a = nodeRequest.socket) === null || _a === void 0 ? void 0 : _a.localPort) {
return (_b = nodeRequest.socket) === null || _b === void 0 ? void 0 : _b.localPort;
}
const portInHeader = (_e = (_d = (_c = nodeRequest.headers) === null || _c === void 0 ? void 0 : _c.host) === null || _d === void 0 ? void 0 : _d.split(':')) === null || _e === void 0 ? void 0 : _e[1];
if (portInHeader) {
return portInHeader;
}
return 80;
}
function getHostnameWithPort(nodeRequest) {
var _a, _b, _c;
if ((_a = nodeRequest.headers) === null || _a === void 0 ? void 0 : _a.host) {
return (_b = nodeRequest.headers) === null || _b === void 0 ? void 0 : _b.host;
}
const port = getPort(nodeRequest);
if (nodeRequest.hostname) {
return nodeRequest.hostname + ':' + port;
}
const localIp = (_c = nodeRequest.socket) === null || _c === void 0 ? void 0 : _c.localAddress;
if (localIp && !(localIp === null || localIp === void 0 ? void 0 : localIp.includes('::')) && !(localIp === null || localIp === void 0 ? void 0 : 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 configureSocket(rawRequest) {
var _a, _b, _c, _d, _e, _f;
(_b = (_a = rawRequest === null || rawRequest === void 0 ? void 0 : rawRequest.socket) === null || _a === void 0 ? void 0 : _a.setTimeout) === null || _b === void 0 ? void 0 : _b.call(_a, 0);
(_d = (_c = rawRequest === null || rawRequest === void 0 ? void 0 : rawRequest.socket) === null || _c === void 0 ? void 0 : _c.setNoDelay) === null || _d === void 0 ? void 0 : _d.call(_c, true);
(_f = (_e = rawRequest === null || rawRequest === void 0 ? void 0 : rawRequest.socket) === null || _e === void 0 ? void 0 : _e.setKeepAlive) === null || _f === void 0 ? void 0 : _f.call(_e, true);
}
function isRequestBody(body) {
const stringTag = body[Symbol.toStringTag];
if (typeof body === 'string' ||
stringTag === 'Uint8Array' ||
stringTag === 'Blob' ||
stringTag === 'FormData' ||
stringTag === 'URLSearchParams' ||
isAsyncIterable(body)) {
return true;
}
return false;
}
function normalizeNodeRequest(nodeRequest, RequestCtor) {
var _a;
const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest;
configureSocket(rawRequest);
let fullUrl = buildFullUrl(rawRequest);
if (nodeRequest.query) {
const urlObj = new URL(fullUrl);
for (const queryName in nodeRequest.query) {
const queryValue = nodeRequest.query[queryName];
urlObj.searchParams.set(queryName, queryValue);
}
fullUrl = urlObj.toString();
}
const baseRequestInit = {
method: nodeRequest.method,
headers: nodeRequest.headers,
};
if (nodeRequest.method === 'GET' || nodeRequest.method === 'HEAD') {
return new RequestCtor(fullUrl, baseRequestInit);
}
/**
* 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, {
...baseRequestInit,
body: maybeParsedBody,
});
}
const request = new RequestCtor(fullUrl, {
...baseRequestInit,
});
if (!((_a = request.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('json'))) {
request.headers.set('content-type', 'application/json');
}
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);
}
},
});
}
return new RequestCtor(fullUrl, {
headers: nodeRequest.headers,
method: nodeRequest.method,
body: rawRequest,
});
}
function isReadable(stream) {
return stream.read != null;
}
function isNodeRequest(request) {
return isReadable(request);
}
function isServerResponse(stream) {
// Check all used functions are defined
return (stream != null && stream.setHeader != null && stream.end != null && stream.once != null && stream.write != null);
}
function isReadableStream(stream) {
return stream != null && stream.getReader != null;
}
function isFetchEvent(event) {
return event != null && event.request != null && event.respondWith != null;
}
async function sendNodeResponse({ headers, status, statusText, body }, serverResponse) {
headers.forEach((value, name) => {
serverResponse.setHeader(name, value);
});
serverResponse.statusCode = status;
serverResponse.statusMessage = statusText;
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
if (body == null) {
serverResponse.end(resolve);
}
else if (body[Symbol.toStringTag] === 'Uint8Array') {
serverResponse
// @ts-expect-error http and http2 writes are actually compatible
.write(body);
serverResponse.end(resolve);
}
else if (isReadable(body)) {
serverResponse.once('close', () => {
body.destroy();
resolve();
});
body.pipe(serverResponse);
}
else if (isAsyncIterable(body)) {
for await (const chunk of body) {
if (!serverResponse
// @ts-expect-error http and http2 writes are actually compatible
.write(chunk)) {
break;
}
}
serverResponse.end(resolve);
}
});
}
function isRequestInit(val) {
return (val != null &&
typeof val === 'object' &&
('body' in val ||
'cache' in val ||
'credentials' in val ||
'headers' in val ||
'integrity' in val ||
'keepalive' in val ||
'method' in val ||
'mode' in val ||
'redirect' in val ||
'referrer' in val ||
'referrerPolicy' in val ||
'signal' in val ||
'window' in val));
}
async function handleWaitUntils(waitUntilPromises) {
const waitUntils = await Promise.allSettled(waitUntilPromises);
waitUntils.forEach(waitUntil => {
if (waitUntil.status === 'rejected') {
console.error(waitUntil.reason);
}
});
}
function createServerAdapter(serverAdapterBaseObject,
/**
* WHATWG Fetch spec compliant `Request` constructor.
*/
RequestCtor = fetch.Request) {
const handleRequest = typeof serverAdapterBaseObject === 'function' ? serverAdapterBaseObject : serverAdapterBaseObject.handle;
function handleNodeRequest(nodeRequest, ...ctx) {
const serverContext = ctx.length > 1 ? Object.assign({}, ...ctx) : ctx[0];
const request = normalizeNodeRequest(nodeRequest, RequestCtor);
return handleRequest(request, serverContext);
}
async function requestListener(nodeRequest, serverResponse, ...ctx) {
const waitUntilPromises = [];
const defaultServerContext = {
req: nodeRequest,
res: serverResponse,
waitUntil(p) {
waitUntilPromises.push(p);
},
};
const response = await handleNodeRequest(nodeRequest, defaultServerContext, ...ctx);
if (response) {
await sendNodeResponse(response, serverResponse);
}
else {
await new Promise(resolve => {
serverResponse.statusCode = 404;
serverResponse.end(resolve);
});
}
if (waitUntilPromises.length > 0) {
await handleWaitUntils(waitUntilPromises);
}
}
function handleEvent(event, ...ctx) {
if (!event.respondWith || !event.request) {
throw new TypeError(`Expected FetchEvent, got ${event}`);
}
const serverContext = ctx.length > 0 ? Object.assign({}, event, ...ctx) : event;
const response$ = handleRequest(event.request, serverContext);
event.respondWith(response$);
}
function handleRequestWithWaitUntil(request, ...ctx) {
const serverContext = ctx.length > 1 ? Object.assign({}, ...ctx) : ctx[0] || {};
if (!('waitUntil' in serverContext)) {
const waitUntilPromises = [];
const response$ = handleRequest(request, {
...serverContext,
waitUntil(p) {
waitUntilPromises.push(p);
},
});
if (waitUntilPromises.length > 0) {
return handleWaitUntils(waitUntilPromises).then(() => response$);
}
return response$;
}
return handleRequest(request, serverContext);
}
const fetchFn = (input, ...maybeCtx) => {
if (typeof input === 'string' || input instanceof URL) {
const [initOrCtx, ...restOfCtx] = maybeCtx;
if (isRequestInit(initOrCtx)) {
return handleRequestWithWaitUntil(new RequestCtor(input, initOrCtx), ...restOfCtx);
}
return handleRequestWithWaitUntil(new RequestCtor(input), ...maybeCtx);
}
return handleRequestWithWaitUntil(input, ...maybeCtx);
};
const genericRequestHandler = (input, ...maybeCtx) => {
// If it is a Node request
const [initOrCtxOrRes, ...restOfCtx] = maybeCtx;
if (isNodeRequest(input)) {
if (!isServerResponse(initOrCtxOrRes)) {
throw new TypeError(`Expected ServerResponse, got ${initOrCtxOrRes}`);
}
return requestListener(input, initOrCtxOrRes, ...restOfCtx);
}
if (isServerResponse(initOrCtxOrRes)) {
throw new TypeError('Got Node response without Node request');
}
// Is input a container object over Request?
if (typeof input === 'object' && 'request' in input) {
// Is it FetchEvent?
if (isFetchEvent(input)) {
return handleEvent(input, ...maybeCtx);
}
// In this input is also the context
return handleRequestWithWaitUntil(input.request, input, ...maybeCtx);
}
// Or is it Request itself?
// Then ctx is present and it is the context
return fetchFn(input, ...maybeCtx);
};
const adapterObj = {
handleRequest,
fetch: fetchFn,
handleNodeRequest,
requestListener,
handleEvent,
handle: genericRequestHandler,
};
return new Proxy(genericRequestHandler, {
// It should have all the attributes of the handler function and the server instance
has: (_, prop) => {
return (prop in adapterObj ||
prop in genericRequestHandler ||
(serverAdapterBaseObject && prop in serverAdapterBaseObject));
},
get: (_, prop) => {
const adapterProp = adapterObj[prop];
if (adapterProp) {
if (adapterProp.bind) {
return adapterProp.bind(adapterObj);
}
return adapterProp;
}
const handleProp = genericRequestHandler[prop];
if (handleProp) {
if (handleProp.bind) {
return handleProp.bind(genericRequestHandler);
}
return handleProp;
}
if (serverAdapterBaseObject) {
const serverAdapterBaseObjectProp = serverAdapterBaseObject[prop];
if (serverAdapterBaseObjectProp) {
if (serverAdapterBaseObjectProp.bind) {
return serverAdapterBaseObjectProp.bind(serverAdapterBaseObject);
}
return serverAdapterBaseObjectProp;
}
}
},
apply(_, __, args) {
return genericRequestHandler(...args);
},
}); // 😡
}
function getCORSHeadersByRequestAndOptions(request, corsOptions) {
var _a, _b;
const headers = {};
if (corsOptions === false) {
return headers;
}
// If defined origins have '*' or undefined by any means, we should allow all origins
if (corsOptions.origin == null || corsOptions.origin.length === 0 || corsOptions.origin.includes('*')) {
const currentOrigin = request.headers.get('origin');
// If origin is available in the headers, use it
if (currentOrigin != null) {
headers['Access-Control-Allow-Origin'] = currentOrigin;
// Vary by origin because there are multiple origins
headers['Vary'] = 'Origin';
}
else {
headers['Access-Control-Allow-Origin'] = '*';
}
}
else if (typeof corsOptions.origin === 'string') {
// If there is one specific origin is specified, use it directly
headers['Access-Control-Allow-Origin'] = corsOptions.origin;
}
else if (Array.isArray(corsOptions.origin)) {
// If there is only one origin defined in the array, consider it as a single one
if (corsOptions.origin.length === 1) {
headers['Access-Control-Allow-Origin'] = corsOptions.origin[0];
}
else {
const currentOrigin = request.headers.get('origin');
if (currentOrigin != null && corsOptions.origin.includes(currentOrigin)) {
// If origin is available in the headers, use it
headers['Access-Control-Allow-Origin'] = currentOrigin;
// Vary by origin because there are multiple origins
headers['Vary'] = 'Origin';
}
else {
// There is no origin found in the headers, so we should return null
headers['Access-Control-Allow-Origin'] = 'null';
}
}
}
if ((_a = corsOptions.methods) === null || _a === void 0 ? void 0 : _a.length) {
headers['Access-Control-Allow-Methods'] = corsOptions.methods.join(', ');
}
else {
const requestMethod = request.headers.get('access-control-request-method');
if (requestMethod) {
headers['Access-Control-Allow-Methods'] = requestMethod;
}
}
if ((_b = corsOptions.allowedHeaders) === null || _b === void 0 ? void 0 : _b.length) {
headers['Access-Control-Allow-Headers'] = corsOptions.allowedHeaders.join(', ');
}
else {
const requestHeaders = request.headers.get('access-control-request-headers');
if (requestHeaders) {
headers['Access-Control-Allow-Headers'] = requestHeaders;
if (headers['Vary']) {
headers['Vary'] += ', Access-Control-Request-Headers';
}
headers['Vary'] = 'Access-Control-Request-Headers';
}
}
if (corsOptions.credentials != null) {
if (corsOptions.credentials === true) {
headers['Access-Control-Allow-Credentials'] = 'true';
}
}
else if (headers['Access-Control-Allow-Origin'] !== '*') {
headers['Access-Control-Allow-Credentials'] = 'true';
}
if (corsOptions.exposedHeaders) {
headers['Access-Control-Expose-Headers'] = corsOptions.exposedHeaders.join(', ');
}
if (corsOptions.maxAge) {
headers['Access-Control-Max-Age'] = corsOptions.maxAge.toString();
}
return headers;
}
async function getCORSResponseHeaders(request, corsOptionsFactory, serverContext) {
const corsOptions = await corsOptionsFactory(request, serverContext);
return getCORSHeadersByRequestAndOptions(request, corsOptions);
}
function withCORS(obj, options, ResponseCtor = fetch.Response) {
let corsOptionsFactory = () => ({});
if (options != null) {
if (typeof options === 'function') {
corsOptionsFactory = options;
}
else if (typeof options === 'object') {
const corsOptions = {
...options,
};
corsOptionsFactory = () => corsOptions;
}
else if (options === false) {
corsOptionsFactory = () => false;
}
}
async function handleWithCORS(request, serverContext) {
let response;
if (request.method.toUpperCase() === 'OPTIONS') {
response = new ResponseCtor(null, {
status: 204,
});
}
else {
response = await obj.handle(request, serverContext);
}
if (response != null) {
const headers = await getCORSResponseHeaders(request, corsOptionsFactory, serverContext);
for (const headerName in headers) {
response.headers.set(headerName, headers[headerName]);
}
return response;
}
}
return new Proxy(obj, {
get(_, prop, receiver) {
if (prop === 'handle') {
return handleWithCORS;
}
return Reflect.get(obj, prop, receiver);
},
});
}
function createDefaultErrorHandler(ResponseCtor = fetch.Response) {
return function defaultErrorHandler(e) {
return new ResponseCtor(e.stack || e.message || e.toString(), {
status: e.statusCode || e.status || 500,
statusText: e.statusText || 'Internal Server Error',
});
};
}
function withErrorHandling(obj, onError = createDefaultErrorHandler()) {
async function handleWithErrorHandling(request, ctx) {
try {
const res = await obj.handle(request, ctx);
return res;
}
catch (e) {
return onError(e, request, ctx);
}
}
return new Proxy(obj, {
get(obj, prop, receiver) {
if (prop === 'handle') {
return handleWithErrorHandling;
}
return Reflect.get(obj, prop, receiver);
},
});
}
Object.defineProperty(exports, 'Response', {
enumerable: true,
get: function () {
return fetch.Response;
}
});
exports.createDefaultErrorHandler = createDefaultErrorHandler;
exports.createServerAdapter = createServerAdapter;
exports.getCORSHeadersByRequestAndOptions = getCORSHeadersByRequestAndOptions;
exports.isAsyncIterable = isAsyncIterable;
exports.isFetchEvent = isFetchEvent;
exports.isNodeRequest = isNodeRequest;
exports.isReadable = isReadable;
exports.isReadableStream = isReadableStream;
exports.isRequestInit = isRequestInit;
exports.isServerResponse = isServerResponse;
exports.normalizeNodeRequest = normalizeNodeRequest;
exports.sendNodeResponse = sendNodeResponse;
exports.withCORS = withCORS;
exports.withErrorHandling = withErrorHandling;