@nestjs/platform-express
Version:
Nest - modern, fast, powerful node.js web framework (@platform-express)
411 lines (410 loc) • 16.3 kB
JavaScript
import { BadRequestException, HttpStatus, InternalServerErrorException, Logger, StreamableFile, VERSION_NEUTRAL, VersioningType, } from '@nestjs/common';
import cors from 'cors';
import express from 'express';
import * as http from 'http';
import * as https from 'https';
import { pathToRegexp } from 'path-to-regexp';
import { getBodyParserOptions } from './utils/get-body-parser-options.util.js';
import { isFunction, isNil, isObject, isString, isUndefined, } from '@nestjs/common/internal';
import { AbstractHttpAdapter } from '@nestjs/core';
import { RouterMethodFactory, LegacyRouteConverter, } from '@nestjs/core/internal';
/**
* @publicApi
*/
export class ExpressAdapter extends AbstractHttpAdapter {
routerMethodFactory = new RouterMethodFactory();
logger = new Logger(ExpressAdapter.name);
openConnections = new Set();
registeredPrefixes = new Set();
isShuttingDown = false;
onRequestHook;
onResponseHook;
constructor(instance) {
super(instance || express());
this.instance.use((req, res, next) => {
if (this.onResponseHook) {
res.on('finish', () => {
void this.onResponseHook.apply(this, [req, res]);
});
}
if (this.onRequestHook) {
void this.onRequestHook.apply(this, [req, res, next]);
}
else {
next();
}
});
}
setOnRequestHook(onRequestHook) {
this.onRequestHook = onRequestHook;
}
setOnResponseHook(onResponseHook) {
this.onResponseHook = onResponseHook;
}
reply(response, body, statusCode) {
if (!isNil(statusCode)) {
response.status(statusCode);
}
if (isNil(body)) {
return response.send();
}
if (body instanceof StreamableFile) {
this.applyStreamHeaders(response, body);
const stream = body.getStream();
stream.once('error', err => {
body.errorHandler(err, response);
});
return stream
.pipe(response)
.on('error', (err) => body.errorLogger(err));
}
const responseContentType = response.getHeader('Content-Type');
if (typeof responseContentType === 'string' &&
!responseContentType.startsWith('application/json') &&
body?.statusCode >= HttpStatus.BAD_REQUEST) {
this.logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses");
response.setHeader('Content-Type', 'application/json');
}
return isObject(body) ? response.json(body) : response.send(String(body));
}
status(response, statusCode) {
return response.status(statusCode);
}
end(response, message) {
return response.end(message);
}
render(response, view, options) {
return response.render(view, options);
}
redirect(response, statusCode, url) {
return response.redirect(statusCode, url);
}
setErrorHandler(handler, prefix) {
if (prefix) {
const router = express.Router();
router.use(handler);
this.use(prefix, router);
}
// Always mount the error handler at the root as well, so routes living
// outside the global prefix (e.g. "setGlobalPrefix" exclusions or
// root-mounted routes) still pass through the exception layer.
return this.use(handler);
}
setNotFoundHandler(handler, prefix) {
if (prefix) {
this.registeredPrefixes.add(prefix);
const router = express.Router();
router.all('*path', handler);
return this.use(prefix, router);
}
return this.use((req, res, next) => {
// When multiple apps share this adapter, a non-prefixed app's 404
// handler may be registered before a prefixed app's routes. Skip
// requests whose path belongs to another app's prefix so they can
// reach the correct route handlers further in the stack.
const path = req.originalUrl.split(/[?#]/)[0];
for (const registeredPrefix of this.registeredPrefixes) {
// Match on full path segments only, so a prefix of "/api" does not
// swallow unrelated paths such as "/apiary".
if (path === registeredPrefix ||
path.startsWith(`${registeredPrefix}/`)) {
return next();
}
}
return handler(req, res, next);
});
}
isHeadersSent(response) {
return response.headersSent;
}
getHeader(response, name) {
return response.get(name);
}
setHeader(response, name, value) {
return response.set(name, value);
}
appendHeader(response, name, value) {
return response.append(name, value);
}
normalizePath(path) {
try {
const convertedPath = LegacyRouteConverter.tryConvert(path);
// Call "pathToRegexp" to trigger a TypeError if the path is invalid
pathToRegexp(convertedPath);
return convertedPath;
}
catch (e) {
if (e instanceof TypeError) {
LegacyRouteConverter.printError(path);
}
throw e;
}
}
listen(port, ...args) {
return this.httpServer.listen(port, ...args);
}
beforeClose() {
this.isShuttingDown = true;
}
close() {
this.isShuttingDown = true;
this.closeOpenConnections();
if (!this.httpServer) {
return undefined;
}
return new Promise(resolve => this.httpServer.close(resolve));
}
set(...args) {
return this.instance.set(...args);
}
enable(...args) {
return this.instance.enable(...args);
}
disable(...args) {
return this.instance.disable(...args);
}
engine(...args) {
return this.instance.engine(...args);
}
useStaticAssets(path, options) {
if (options && options.prefix) {
return this.use(options.prefix, express.static(path, options));
}
return this.use(express.static(path, options));
}
setBaseViewsDir(path) {
return this.set('views', path);
}
setViewEngine(engine) {
return this.set('view engine', engine);
}
getRequestHostname(request) {
return request.hostname;
}
getRequestMethod(request) {
return request.method;
}
getRequestUrl(request) {
return request.originalUrl;
}
enableCors(options) {
return this.use(cors(options));
}
createMiddlewareFactory(requestMethod) {
return (path, callback) => {
try {
const convertedPath = LegacyRouteConverter.tryConvert(path);
return this.routerMethodFactory
.get(this.instance, requestMethod)
.call(this.instance, convertedPath, callback);
}
catch (e) {
if (e instanceof TypeError) {
LegacyRouteConverter.printError(path);
}
throw e;
}
};
}
initHttpServer(options) {
const isHttpsEnabled = options && options.httpsOptions;
if (isHttpsEnabled) {
this.httpServer = https.createServer(options.httpsOptions, this.getInstance());
}
else {
this.httpServer = http.createServer(this.getInstance());
}
if (options?.return503OnClosing) {
this.instance.use((req, res, next) => {
if (this.isShuttingDown) {
res.set('Connection', 'close');
res.status(503).send('Service Unavailable');
}
else {
next();
}
});
}
if (options?.forceCloseConnections) {
this.trackOpenConnections();
}
}
registerParserMiddleware(prefix, rawBody) {
const bodyParserJsonOptions = getBodyParserOptions('json', rawBody);
const bodyParserUrlencodedOptions = getBodyParserOptions('urlencoded', rawBody, { extended: true });
const parserMiddleware = {
jsonParser: express.json(bodyParserJsonOptions),
urlencodedParser: express.urlencoded(bodyParserUrlencodedOptions),
};
Object.keys(parserMiddleware)
.filter(parser => !this.isMiddlewareApplied(parser))
.forEach(parserKey => this.use(parserMiddleware[parserKey]));
}
useBodyParser(type, rawBody, options) {
const parserOptions = getBodyParserOptions(type, rawBody, options);
const parser = express[type](parserOptions);
this.use(parser);
return this;
}
setLocal(key, value) {
this.instance.locals[key] = value;
return this;
}
getType() {
return 'express';
}
isRouteOrderSensitive() {
return true;
}
applyVersionFilter(handler, version, versioningOptions) {
const callNextHandler = (req, res, next) => {
if (!next) {
throw new InternalServerErrorException('HTTP adapter does not support filtering on version');
}
return next();
};
if (version === VERSION_NEUTRAL ||
// URL Versioning is done via the path, so the filter continues forward
versioningOptions.type === VersioningType.URI) {
const handlerForNoVersioning = (req, res, next) => handler(req, res, next);
return handlerForNoVersioning;
}
// Custom Extractor Versioning Handler
if (versioningOptions.type === VersioningType.CUSTOM) {
const handlerForCustomVersioning = (req, res, next) => {
const extractedVersion = versioningOptions.extractor(req);
if (Array.isArray(version)) {
if (Array.isArray(extractedVersion) &&
version.filter(v => extractedVersion.includes(v)).length) {
return handler(req, res, next);
}
if (isString(extractedVersion) &&
version.includes(extractedVersion)) {
return handler(req, res, next);
}
}
else if (isString(version)) {
// Known bug here - if there are multiple versions supported across separate
// handlers/controllers, we can't select the highest matching handler.
// Since this code is evaluated per-handler, then we can't see if the highest
// specified version exists in a different handler.
if (Array.isArray(extractedVersion) &&
extractedVersion.includes(version)) {
return handler(req, res, next);
}
if (isString(extractedVersion) && version === extractedVersion) {
return handler(req, res, next);
}
}
return callNextHandler(req, res, next);
};
return handlerForCustomVersioning;
}
// Media Type (Accept Header) Versioning Handler
if (versioningOptions.type === VersioningType.MEDIA_TYPE) {
const handlerForMediaTypeVersioning = (req, res, next) => {
const MEDIA_TYPE_HEADER = 'Accept';
const acceptHeaderValue = req.headers?.[MEDIA_TYPE_HEADER] ||
req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()];
const acceptHeaderVersionParameter = acceptHeaderValue
? acceptHeaderValue.split(';')[1]
: undefined;
// No version was supplied
if (isUndefined(acceptHeaderVersionParameter)) {
if (Array.isArray(version)) {
if (version.includes(VERSION_NEUTRAL)) {
return handler(req, res, next);
}
}
}
else {
const headerVersion = acceptHeaderVersionParameter.split(versioningOptions.key)[1];
if (Array.isArray(version)) {
if (version.includes(headerVersion)) {
return handler(req, res, next);
}
}
else if (isString(version)) {
if (version === headerVersion) {
return handler(req, res, next);
}
}
}
return callNextHandler(req, res, next);
};
return handlerForMediaTypeVersioning;
}
// Header Versioning Handler
if (versioningOptions.type === VersioningType.HEADER) {
const handlerForHeaderVersioning = (req, res, next) => {
const customHeaderVersionParameter = req.headers?.[versioningOptions.header] ||
req.headers?.[versioningOptions.header.toLowerCase()];
// No version was supplied
if (isUndefined(customHeaderVersionParameter)) {
if (Array.isArray(version)) {
if (version.includes(VERSION_NEUTRAL)) {
return handler(req, res, next);
}
}
}
else {
if (Array.isArray(version)) {
if (version.includes(customHeaderVersionParameter)) {
return handler(req, res, next);
}
}
else if (isString(version)) {
if (version === customHeaderVersionParameter) {
return handler(req, res, next);
}
}
}
return callNextHandler(req, res, next);
};
return handlerForHeaderVersioning;
}
throw new Error('Unsupported versioning options');
}
mapException(error) {
switch (true) {
// SyntaxError is thrown by Express body-parser when given invalid JSON (#422, #430)
// URIError is thrown by Express when given a path parameter with an invalid percentage
// encoding, e.g. '%FF' (#8915)
case error instanceof SyntaxError || error instanceof URIError:
return new BadRequestException(error.message);
default:
return error;
}
}
trackOpenConnections() {
this.httpServer.on('connection', (socket) => {
this.openConnections.add(socket);
socket.on('close', () => this.openConnections.delete(socket));
});
}
closeOpenConnections() {
for (const socket of this.openConnections) {
socket.destroy();
this.openConnections.delete(socket);
}
}
isMiddlewareApplied(name) {
const app = this.getInstance();
return (!!app.router &&
!!app.router.stack &&
isFunction(app.router.stack.filter) &&
app.router.stack.some((layer) => layer && layer.handle && layer.handle.name === name));
}
setHeaderIfNotExists(response, name, value) {
if (value !== undefined && response.getHeader(name) === undefined) {
const headerValue = Array.isArray(value) ? value.join(',') : value;
response.setHeader(name, headerValue);
}
}
applyStreamHeaders(response, streamable) {
const headers = streamable.getHeaders();
this.setHeaderIfNotExists(response, 'Content-Type', headers.type);
this.setHeaderIfNotExists(response, 'Content-Disposition', headers.disposition);
this.setHeaderIfNotExists(response, 'Content-Length', headers.length);
}
}