UNPKG

@nestjs/platform-express

Version:

Nest - modern, fast, powerful node.js web framework (@platform-express)

411 lines (410 loc) 16.3 kB
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); } }