UNPKG

@nestjs/platform-fastify

Version:

Run NestJS on Fastify: the HTTP adapter with Fastify's plugins, JSON schemas and serialization.

1,281 lines • 56.1 kB
import { HttpException, HttpStatus, Logger, StreamableFile, VERSION_NEUTRAL, VersioningType, } from '@nestjs/common';
import { fastify, } from 'fastify';
import Reply from 'fastify/lib/reply.js';
import fastifySymbols from 'fastify/lib/symbols.js';
import * as http from 'http';
import * as net from 'net';
import { pathToRegexp } from 'path-to-regexp';
import { Readable } from 'stream';
import middie from '@fastify/middie';
import { loadPackage, tryLoadPackage, isFunction, isNil, isObject, isString, isUndefined, } from '@nestjs/common/internal';
import { AbstractHttpAdapter } from '@nestjs/core';
import { LegacyRouteConverter } from '@nestjs/core/internal';
import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
const { kReplySerializer, kRouteContext } = fastifySymbols;
// Fastify uses `fast-querystring` internally to quickly parse URL query strings.
import { parse as querystringParse } from 'fast-querystring';
import urlSanitizer from 'find-my-way/lib/url-sanitizer.js';
import { FASTIFY_ROUTE_CONFIG_METADATA, FASTIFY_ROUTE_CONSTRAINTS_METADATA, FASTIFY_ROUTE_SCHEMA_METADATA, } from '../constants.js';
const { safeDecodeURI } = urlSanitizer;
const MISSING_MULTIPART_PACKAGE_MESSAGE = 'The "@fastify/multipart" package is missing. Please, make sure to install it (npm i @fastify/multipart) ' +
    'to use the file upload interceptors from "@nestjs/platform-fastify/multipart".';
function isFastifyMultipartPlugin(plugin) {
    const fn = plugin?.default ?? plugin;
    return (typeof fn === 'function' &&
        (fn[Symbol.for('plugin-meta')]?.name === '@fastify/multipart' ||
            fn.name === 'fastifyMultipart'));
}
// Covers "application/json" and the "+json" structured syntax suffix (RFC 6839),
// e.g. "application/problem+json", with or without parameters.
function isJsonContentType(contentType) {
    const mediaType = contentType.split(';')[0].trim().toLowerCase();
    return (mediaType.startsWith('application/json') || mediaType.endsWith('+json'));
}
// A custom error body need not have a "statusCode" of its own. Fastify sends
// streams, Responses and binary data as they are, and other objects through
// the serializer of `reply.serializer()`. Without one, it answers 500 for an
// object sent with a non-JSON Content-Type.
function isErrorBody(body, statusCode, reply) {
    if (body?.statusCode >= HttpStatus.BAD_REQUEST) {
        return true;
    }
    return (!isNil(statusCode) &&
        statusCode >= HttpStatus.BAD_REQUEST &&
        isObject(body) &&
        !isFunction(body.pipe) &&
        !isFunction(body.getReader) &&
        Object.prototype.toString.call(body) !== '[object Response]' &&
        !ArrayBuffer.isView(body) &&
        isNil(reply[kReplySerializer]));
}
/**
 * @publicApi
 */
export class FastifyAdapter extends AbstractHttpAdapter {
    logger = new Logger(FastifyAdapter.name);
    _pathPrefix;
    openConnections = new Set();
    isClosing = false;
    isShuttingDown = false;
    return503OnClosing = false;
    _isParserRegistered;
    // Fastify's `hasContentTypeParser('application/json')` is always true (built-in
    // parser), so the types registered through `useBodyParser()` are tracked here.
    registeredContentTypes = new Set();
    onRequestHook;
    onResponseHook;
    securityHook;
    // The request hook `@fastify/cors` adds for `app.enableCors()`, which the
    // constructor's hook runs (see `enableCors()`)
    corsHook;
    isMiddieRegistered;
    multipartMode = 'auto';
    multipartOptions = {};
    isMultipartRequested = false;
    pendingMiddlewares = [];
    // The encapsulated contexts created since middie was registered (see
    // `registerMiddie()`)
    middieContexts = [];
    versioningOptions;
    // Exceptions thrown by the extractor of custom versioning, by raw request
    versionExtractorErrors = new WeakMap();
    versionConstraint = {
        name: 'version',
        validate(value) {
            if (!isString(value) && !Array.isArray(value)) {
                throw new Error('Version constraint should be a string or an array of strings.');
            }
        },
        storage() {
            const versions = new Map();
            return {
                get(version) {
                    if (Array.isArray(version)) {
                        return versions.get(version.find(v => versions.has(v))) || null;
                    }
                    return versions.get(version) || null;
                },
                set(versionOrVersions, store) {
                    const storeVersionConstraint = (version) => versions.set(version, store);
                    if (Array.isArray(versionOrVersions))
                        versionOrVersions.forEach(storeVersionConstraint);
                    else
                        storeVersionConstraint(versionOrVersions);
                },
                del(version) {
                    if (Array.isArray(version)) {
                        version.forEach(v => versions.delete(v));
                    }
                    else {
                        versions.delete(version);
                    }
                },
                empty() {
                    versions.clear();
                },
            };
        },
        deriveConstraint: (req) => {
            // Media Type (Accept Header) Versioning Handler
            if (this.versioningOptions?.type === VersioningType.MEDIA_TYPE) {
                const MEDIA_TYPE_HEADER = 'Accept';
                const acceptHeaderValue = req.headers?.[MEDIA_TYPE_HEADER] ||
                    req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()];
                const headerVersion = getMediaTypeVersion(acceptHeaderValue, this.versioningOptions.key);
                return isUndefined(headerVersion)
                    ? VERSION_NEUTRAL // No version was supplied
                    : headerVersion;
            }
            // Header Versioning Handler
            else if (this.versioningOptions?.type === VersioningType.HEADER) {
                const customHeaderVersionParameter = req.headers?.[this.versioningOptions.header] ||
                    req.headers?.[this.versioningOptions.header.toLowerCase()];
                return isUndefined(customHeaderVersionParameter)
                    ? VERSION_NEUTRAL // No version was supplied
                    : customHeaderVersionParameter;
            }
            // Custom Versioning Handler
            else if (this.versioningOptions?.type === VersioningType.CUSTOM) {
                try {
                    return this.versioningOptions.extractor(req);
                }
                catch (err) {
                    // Fastify routes the request, and so calls the extractor, before
                    // anything could catch the exception. It is passed to the error
                    // handler from an `onRequest` hook instead (see the constructor).
                    this.versionExtractorErrors.set(req, err);
                    return undefined;
                }
            }
            return undefined;
        },
        mustMatchWhenDerived: false,
    };
    get isParserRegistered() {
        return !!this._isParserRegistered;
    }
    constructor(instanceOrOptions) {
        super();
        const instance = instanceOrOptions && instanceOrOptions.server
            ? instanceOrOptions
            : fastify({
                // The router answers a malformed URL (400) or an over-long route
                // parameter (414) before any hook runs, so the security hook (see
                // `registerSecurityHook()`) runs here, for its headers.
                frameworkErrors: (error, request, reply) => {
                    try {
                        this.securityHook?.(request, reply.raw);
                    }
                    catch {
                        // The request is rejected either way, and a throw here would
                        // escape the router. `request.params` and `request.query`
                        // are null, so a CSRF `exclude` predicate may throw.
                    }
                    reply.send(error);
                },
                ...instanceOrOptions,
                routerOptions: {
                    ...this.getTopLevelRouterOptions(instanceOrOptions),
                    ...instanceOrOptions?.routerOptions,
                    constraints: {
                        ...instanceOrOptions?.constraints,
                        ...instanceOrOptions?.routerOptions
                            ?.constraints,
                        version: this.versionConstraint,
                    },
                },
            });
        this.setInstance(instance);
        if (instanceOrOptions?.skipMiddie) {
            this.isMiddieRegistered = true;
        }
        const multipart = instanceOrOptions
            ?.multipart;
        if (multipart === false) {
            this.multipartMode = 'off';
        }
        else if (multipart) {
            this.multipartMode = 'eager';
            this.multipartOptions = multipart === true ? {} : { ...multipart };
        }
        this.instance.addHook('onRequest', (request, reply, done) => {
            if (this.onRequestHook) {
                this.runLifecycleHook(this.onRequestHook, request, reply, done);
            }
            else {
                done();
            }
        });
        // Fastify's own `return503OnClosing` only applies once `close()` runs,
        // which is after the shutdown hooks. Added here rather than in
        // `initHttpServer()` so that it runs before Nest middleware:
        // `NestFactory.create()` calls `init()`, which registers middie, first.
        // Answers with the same body as Fastify's own 503, so that clients get
        // one response shape for the whole shutdown.
        this.instance.addHook('onRequest', (request, reply, done) => {
            if (this.return503OnClosing && this.isShuttingDown) {
                if (request.raw.httpVersionMajor !== 2) {
                    reply.header('Connection', 'close');
                }
                reply.code(503).send({
                    error: 'Service Unavailable',
                    message: 'Service Unavailable',
                    statusCode: 503,
                });
            }
            else {
                done();
            }
        });
        // The request hook of `app.enableCors()` (see `enableCors()`). Added here
        // for the same reason as the hook above: it must run before Nest
        // middleware, so that a preflight is answered before them and the
        // responses they end carry the CORS headers, as on Express. Before the
        // security hook, so that its responses carry them too.
        this.instance.addHook('onRequest', (request, reply, done) => {
            if (!this.corsHook) {
                return done();
            }
            this.corsHook(request, reply, (err) => {
                this.copyCorsHeadersToRaw(reply);
                done(err);
            });
        });
        // The request hook of `app.enableCsrfProtection()` and
        // `app.useSecurityHeaders()` (see `registerSecurityHook()`). Added here
        // for the same reason as the hook above: it must run before Nest
        // middleware, and `NestFactory.create()` registers middie before either
        // method can be called. After the `503` hook, as on Express.
        this.instance.addHook('onRequest', (request, reply, done) => {
            if (this.securityHook) {
                done(this.securityHook(request, reply.raw));
            }
            else {
                done();
            }
        });
        // Passes requests whose target middleware cannot be matched against to
        // the not-found handler (see `isRoutableRequestTarget()`). Added here,
        // like the hooks above, so that it runs before Nest middleware, and
        // after the security hook, so that the `404` carries its headers, as on
        // Express. A request the router did not match is already on its way to
        // the not-found handler.
        this.instance.addHook('onRequest', (request, reply, done) => {
            if (request.is404 || this.isRoutableRequestTarget(request.raw.url)) {
                done();
            }
            else {
                reply.callNotFound();
            }
        });
        // Passes an exception thrown by the extractor of custom versioning while
        // Fastify routed the request (see `versionConstraint`) to the error
        // handler, for matched and unmatched routes alike. Added here, like the
        // hooks above, so that it runs before Nest middleware.
        this.instance.addHook('onRequest', (request, _reply, done) => {
            if (!this.versionExtractorErrors.has(request.raw)) {
                done();
                return;
            }
            const err = this.versionExtractorErrors.get(request.raw);
            this.versionExtractorErrors.delete(request.raw);
            done(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR());
        });
        this.instance.addHook('onResponse', (request, reply, done) => {
            if (this.onResponseHook) {
                this.runLifecycleHook(this.onResponseHook, request, reply, done);
            }
            else {
                done();
            }
        });
    }
    runLifecycleHook(hook, request, reply, done) {
        let completed = false;
        const doneOnce = (err) => {
            if (completed)
                return;
            completed = true;
            done(err);
        };
        try {
            // Do not return the promise to Fastify: both completion styles share
            // this guard, including hooks that call done and return a promise.
            const result = hook.call(this, request, reply, doneOnce);
            if (result && typeof result.then === 'function') {
                result.then(() => doneOnce(), err => doneOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR()));
            }
        }
        catch (err) {
            doneOnce(err);
        }
    }
    setOnRequestHook(hook) {
        this.onRequestHook = hook;
    }
    setOnResponseHook(hook) {
        this.onResponseHook = hook;
    }
    async init() {
        if (this.multipartMode === 'eager') {
            this.useMultipart();
        }
        if (this.isMiddieRegistered) {
            return;
        }
        await this.registerMiddie();
        // Register any pending middlewares that were added before init
        if (this.pendingMiddlewares.length > 0) {
            for (const { args } of this.pendingMiddlewares) {
                this.instance.use(...args);
            }
            this.pendingMiddlewares = [];
        }
    }
    listen(listenOptions, ...args) {
        const isFirstArgTypeofFunction = typeof args[0] === 'function';
        const callback = isFirstArgTypeofFunction ? args[0] : args[1];
        if (typeof listenOptions === 'string' && Number.isNaN(+listenOptions)) {
            // A string that is not a number is a Unix socket path or a named pipe.
            // The host does not apply to it.
            return this.instance.listen({ path: listenOptions }, callback);
        }
        let options;
        if (typeof listenOptions === 'object' &&
            (listenOptions.host !== undefined ||
                listenOptions.port !== undefined ||
                listenOptions.path !== undefined ||
                listenOptions.fd !== undefined)) {
            // First parameter is an object with a path, port, fd and/or host attributes
            options = listenOptions;
        }
        else {
            options = {
                port: +listenOptions,
            };
        }
        if (!isFirstArgTypeofFunction) {
            options.host = args[0];
        }
        if (options.port !== undefined && !this.isValidPort(options.port)) {
            // Fastify calls `server.listen()` from its ready callback, where the
            // RangeError Node throws for a bad port becomes an unhandled rejection
            // and the callback never runs. Throw right away instead, as the
            // `http.Server` of the Express adapter does.
            const error = new RangeError(`Options.port should be >= 0 and < 65536. Received ${String(options.port)}.`);
            throw Object.assign(error, { code: 'ERR_SOCKET_BAD_PORT' });
        }
        return this.instance.listen(options, callback);
    }
    isValidPort(port) {
        const value = typeof port === 'string' && port.trim() !== '' ? Number(port) : port;
        return (typeof value === 'number' &&
            Number.isInteger(value) &&
            value >= 0 &&
            value <= 65535);
    }
    get(...args) {
        return this.injectRouteOptions('GET', ...args);
    }
    post(...args) {
        return this.injectRouteOptions('POST', ...args);
    }
    head(...args) {
        return this.injectRouteOptions('HEAD', ...args);
    }
    delete(...args) {
        return this.injectRouteOptions('DELETE', ...args);
    }
    put(...args) {
        return this.injectRouteOptions('PUT', ...args);
    }
    patch(...args) {
        return this.injectRouteOptions('PATCH', ...args);
    }
    options(...args) {
        return this.injectRouteOptions('OPTIONS', ...args);
    }
    search(...args) {
        return this.injectRouteOptions('SEARCH', ...args);
    }
    query(...args) {
        return this.injectRouteOptions('QUERY', ...args);
    }
    propfind(...args) {
        return this.injectRouteOptions('PROPFIND', ...args);
    }
    proppatch(...args) {
        return this.injectRouteOptions('PROPPATCH', ...args);
    }
    mkcol(...args) {
        return this.injectRouteOptions('MKCOL', ...args);
    }
    copy(...args) {
        return this.injectRouteOptions('COPY', ...args);
    }
    move(...args) {
        return this.injectRouteOptions('MOVE', ...args);
    }
    lock(...args) {
        return this.injectRouteOptions('LOCK', ...args);
    }
    unlock(...args) {
        return this.injectRouteOptions('UNLOCK', ...args);
    }
    all(...args) {
        // Routes registered by Nest ("path, handler") get the route options
        // (e.g. the version), with the same methods as "fastify.all()".
        if (args.length === 2 && isFunction(args[1])) {
            return this.injectRouteOptions(this.instance.supportedMethods, ...args);
        }
        const handlerRef = args[args.length - 1];
        args[args.length - 1] = this.requireNonEmptyParams(args[0], handlerRef);
        return this.instance.all(...args);
    }
    applyVersionFilter(handler, version, versioningOptions) {
        if (!this.versioningOptions) {
            this.versioningOptions = versioningOptions;
        }
        const versionedRoute = handler;
        versionedRoute.version = version;
        return versionedRoute;
    }
    reply(response, body, statusCode) {
        const fastifyReply = this.isNativeResponse(response)
            ? new Reply(response, {
                [kRouteContext]: {
                    preSerialization: null,
                    preValidation: [],
                    preHandler: [],
                    onSend: [],
                    onError: [],
                },
            }, {})
            : response;
        if (!isNil(statusCode)) {
            fastifyReply.status(statusCode);
        }
        if (body instanceof StreamableFile) {
            const stream = body.getStream();
            // Once the reply was sent (e.g. by `handlerTimeout`), send() drops the
            // stream without consuming or destroying it.
            if (fastifyReply.sent && isFunction(stream?.destroy)) {
                stream.destroy();
            }
            const streamHeaders = body.getHeaders();
            if (fastifyReply.getHeader('Content-Type') === undefined &&
                streamHeaders.type !== undefined) {
                fastifyReply.header('Content-Type', streamHeaders.type);
            }
            if (fastifyReply.getHeader('Content-Disposition') === undefined &&
                streamHeaders.disposition !== undefined) {
                fastifyReply.header('Content-Disposition', streamHeaders.disposition);
            }
            // A "Transfer-Encoding" forbids a "Content-Length" (RFC 9112)
            if (fastifyReply.getHeader('Content-Length') === undefined &&
                fastifyReply.getHeader('Transfer-Encoding') === undefined &&
                streamHeaders.length !== undefined) {
                fastifyReply.header('Content-Length', streamHeaders.length);
            }
            body = stream;
            // Fastify's HEAD route handler resumes a stream payload, and a route
            // declared for HEAD (or a raw response) pipes it into a response that
            // discards it, so the whole source would be read for nothing. Fastify
            // gets an empty stream instead, as an empty body would reset
            // Content-Length to 0.
            if (fastifyReply.raw?.req?.method === 'HEAD' &&
                isFunction(stream?.destroy)) {
                stream.destroy();
                body = Readable.from([]);
            }
        }
        // Fastify passes an Error payload to the error handler instead of
        // sending it, and the reply around a raw response (e.g. the one
        // middleware gets) has none. An Error body sent with an error status,
        // as for `new UnauthorizedException(err)`, gets the standard body of the
        // status instead, without the properties of the Error.
        if (body instanceof Error &&
            fastifyReply.statusCode >= HttpStatus.BAD_REQUEST) {
            body = {
                statusCode: fastifyReply.statusCode,
                message: http.STATUS_CODES[fastifyReply.statusCode] ?? 'Error',
            };
        }
        const responseContentType = fastifyReply.getHeader('Content-Type');
        if (typeof responseContentType === 'string' &&
            !isJsonContentType(responseContentType) &&
            isErrorBody(body, statusCode, fastifyReply)) {
            Logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses", FastifyAdapter.name);
            fastifyReply.header('Content-Type', 'application/json');
        }
        return fastifyReply.send(body);
    }
    status(response, statusCode) {
        if (this.isNativeResponse(response)) {
            response.statusCode = statusCode;
            return response;
        }
        return response.code(statusCode);
    }
    end(response, message) {
        if (this.isNativeResponse(response)) {
            response.end(message);
            return;
        }
        const reply = response;
        reply.raw.end(message);
    }
    render(response, view, options) {
        return response && response.view(view, options);
    }
    redirect(response, statusCode, url) {
        const code = statusCode ?? HttpStatus.FOUND;
        return response.status(code).redirect(url);
    }
    setErrorHandler(handler) {
        return this.instance.setErrorHandler(handler);
    }
    setNotFoundHandler(handler) {
        return this.instance.setNotFoundHandler(handler);
    }
    getHttpServer() {
        return this.instance.server;
    }
    getInstance() {
        return this.instance;
    }
    register(plugin, opts) {
        if (this.multipartMode !== 'off' &&
            isFastifyMultipartPlugin(plugin) &&
            !this.instance.hasRequestDecorator('isMultipart')) {
            // The adapter owns the "@fastify/multipart" registration, so that the
            // plugin is registered once whether or not the user registers it too.
            // The user's options apply over the adapter's.
            this.multipartOptions = { ...this.multipartOptions, ...opts };
            this.useMultipart();
            return this.instance;
        }
        return this.instance.register(plugin, opts);
    }
    /**
     * Registers the "@fastify/multipart" plugin, unless the adapter's
     * `multipart` option is `false`, or the plugin has been registered
     * already by the time it loads. Idempotent. Called by the upload
     * interceptors from "@nestjs/platform-fastify/multipart".
     *
     * The plugin is loaded lazily, and a missing package fails the
     * application's startup (`ready()`) with an error naming the package,
     * rather than exiting the process.
     */
    useMultipart() {
        if (this.multipartMode === 'off' || this.isMultipartRequested) {
            return;
        }
        this.isMultipartRequested = true;
        const registerMultipart = async (instance) => {
            // Plugins load in registration order, so one the user registered
            // before this point has loaded by now.
            if (instance.hasRequestDecorator('isMultipart')) {
                return;
            }
            const multipart = await tryLoadPackage('@fastify/multipart', () => import('@fastify/multipart'));
            if (!multipart) {
                throw new Error(MISSING_MULTIPART_PACKAGE_MESSAGE);
            }
            // Copied: the plugin writes its defaults into the options it receives.
            const { limits, ...options } = this.multipartOptions;
            await instance.register(multipart, {
                ...options,
                ...(limits && { limits: { ...limits } }),
            });
        };
        // Like `fastify-plugin`: register on the root context, not a child one.
        Object.assign(registerMultipart, {
            [Symbol.for('skip-override')]: true,
            [Symbol.for('fastify.display-name')]: 'nestjs-multipart',
        });
        this.instance.register(registerMultipart);
    }
    inject(opts) {
        return this.instance.inject(opts);
    }
    beforeClose() {
        this.isShuttingDown = true;
    }
    async close() {
        this.isShuttingDown = true;
        this.isClosing = true;
        try {
            this.closeOpenConnections();
        }
        finally {
            await this.instance.close().catch(err => {
                // Check if server is still running
                if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
                    throw err;
                }
            });
        }
    }
    initHttpServer(options = {}) {
        this.httpServer = this.instance.server;
        this.return503OnClosing = !!options?.return503OnClosing;
        if (options?.forceCloseConnections) {
            this.trackOpenConnections();
        }
    }
    // `register()` accepts a promise of a plugin, so the import is handed over
    // unawaited on purpose. `NestApplication.useStaticAssets()` discards what
    // this returns, so awaiting here would enqueue the plugin after the caller
    // has already moved on to `listen()`.
    useStaticAssets(options) {
        return this.register(loadPackage('@fastify/static', 'FastifyAdapter.useStaticAssets()', () => import('@fastify/static')), options);
    }
    // Handed over unawaited for the same reason as `useStaticAssets()` above.
    setViewEngine(options) {
        if (isString(options)) {
            new Logger('FastifyAdapter').error("setViewEngine() doesn't support a string argument.");
            process.exit(1);
        }
        return this.register(loadPackage('@fastify/view', 'FastifyAdapter.setViewEngine()', () => import('@fastify/view')), options);
    }
    isHeadersSent(response) {
        if (this.isNativeResponse(response)) {
            return response.headersSent;
        }
        const reply = response;
        // `reply.sent` only covers hijacked or ended replies, not headers
        // already flushed through `reply.raw`.
        return reply.sent || reply.raw.headersSent;
    }
    getHeader(response, name) {
        return response.getHeader(name);
    }
    setHeader(response, name, value) {
        if (this.isNativeResponse(response)) {
            return response.setHeader(name, value);
        }
        const reply = response;
        return reply.header(name, value);
    }
    appendHeader(response, name, value) {
        if (this.isNativeResponse(response)) {
            const current = response.getHeader(name);
            return response.setHeader(name, current === undefined ? value : [current, value].flat().map(String));
        }
        const reply = response;
        // A set-cookie value can live on the reply or only on the raw response
        // (e.g., set by a middleware). Reply.getHeader() reads both, and the
        // merged list is written to both: reply.send() sends the reply headers,
        // while a hijacked reply or a handler that ends reply.raw sends the raw
        // ones.
        if (String(name).toLowerCase() === 'set-cookie') {
            const current = reply.getHeader(name);
            const values = current === undefined ? [value] : [current, value].flat().map(String);
            reply.removeHeader(name);
            reply.header(name, values);
            if (!reply.raw.headersSent) {
                // A copy, so a later reply.header() call does not change the raw header
                reply.raw.setHeader(name, [...values]);
            }
            return reply;
        }
        const current = reply.getHeader(name);
        if (current === undefined) {
            return reply.header(name, value);
        }
        const values = Array.isArray(current) ? current : [current];
        return reply.header(name, values.concat(value));
    }
    getRequestHostname(request) {
        return request.hostname;
    }
    getRequestMethod(request) {
        return request.raw ? request.raw.method : request.method;
    }
    getRequestUrl(request) {
        return this.getRequestOriginalUrl(request.raw || request);
    }
    enableCors(options) {
        const hook = options?.hook;
        if (hook && hook !== 'onRequest') {
            // A later hook runs after Nest middleware wherever it is added
            this.register(import('@fastify/cors'), options);
            return;
        }
        const registerCors = async (instance) => {
            const { default: fastifyCors } = await import('@fastify/cors');
            // `@fastify/cors` adds its request hook to the instance it is given,
            // which would run after middie: the hook is kept so that the hook added
            // in the constructor runs it instead
            const corsInstance = new Proxy(instance, {
                get: (target, prop) => {
                    if (prop === 'addHook') {
                        return (name, hookFn) => {
                            if (name === 'onRequest' && !this.corsHook) {
                                this.corsHook = hookFn;
                                return corsInstance;
                            }
                            return target.addHook.call(target, name, hookFn);
                        };
                    }
                    const value = Reflect.get(target, prop, target);
                    return isFunction(value) ? value.bind(target) : value;
                },
            });
            // As `register()` would, call a function that takes the instance for
            // the options; one that takes `(request, callback)` is the options
            // delegate of `@fastify/cors` (and the `cors` option)
            const corsOptions = isFunction(options) && options.length < 2
                ? options(instance)
                : options;
            await new Promise((resolve, reject) => fastifyCors(corsInstance, corsOptions ?? {}, err => (err ? reject(err) : resolve())));
        };
        // Like `fastify-plugin`: register on the root context, not a child one.
        Object.assign(registerCors, {
            [Symbol.for('skip-override')]: true,
            [Symbol.for('fastify.display-name')]: 'nestjs-cors',
        });
        this.instance.register(registerCors);
    }
    /**
     * Copies the headers `@fastify/cors` set on the reply to the Node.js
     * response, so that a response a middleware ends itself, or the error
     * response of a middleware that throws (sent through a new reply), carries
     * them too. Fastify merges them into the response it sends, in which the
     * reply's own values win.
     */
    copyCorsHeadersToRaw(reply) {
        if (reply.raw.headersSent) {
            return;
        }
        const headers = reply.getHeaders();
        for (const name of Object.keys(headers)) {
            const value = headers[name];
            if (!isUndefined(value) &&
                (name === 'vary' || name.startsWith('access-control-'))) {
                reply.raw.setHeader(name, value);
            }
        }
    }
    /**
     * Runs the request hook of the built-in HTTP security features in an
     * `onRequest` hook added by the constructor: before middie (Nest middleware
     * and `app.use()` middleware, whenever they are registered), content-type
     * parsing, guards and handlers, and also for unmatched routes. Headers set
     * by the hook go to the Node.js response (`reply.raw`): Fastify merges them
     * into every response it sends, errors and `404`s included, while
     * `reply.header()` / `@Header()` values take precedence, and responses
     * written to `reply.raw` directly (e.g. `@Sse()`, or middleware) carry them
     * too. A rejection goes to `done(error)`, i.e. to the Nest exception layer
     * installed with `setErrorHandler()`.
     */
    registerSecurityHook(hook) {
        this.securityHook = hook;
    }
    registerParserMiddleware(prefix, rawBody) {
        if (this._isParserRegistered) {
            return;
        }
        this.registerUrlencodedContentParser(rawBody);
        this.registerJsonContentParser(rawBody);
        this._isParserRegistered = true;
        this._pathPrefix = prefix
            ? !prefix.startsWith('/')
                ? `/${prefix}`
                : prefix
            : undefined;
    }
    useBodyParser(type, rawBody, options, parser) {
        if (Array.isArray(type)) {
            // One by one, so that each content type gets its own default parser.
            for (const contentType of type) {
                this.useBodyParser(contentType, rawBody, options, parser);
            }
            return;
        }
        const parserOptions = {
            ...(options || {}),
            parseAs: 'buffer',
        };
        const bodyParser = parser ?? this.getDefaultBodyParser(type);
        this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
            if (rawBody === true && Buffer.isBuffer(body)) {
                req.rawBody = body;
            }
            if (bodyParser) {
                bodyParser(req, body, done);
                return;
            }
            done(null, body);
        });
        this.registeredContentTypes.add(this.normalizeContentType(type));
    }
    async createMiddlewareFactory(requestMethod) {
        if (!this.isMiddieRegistered) {
            await this.registerMiddie();
        }
        return (path, callback) => {
            const hasEndOfStringCharacter = path.endsWith('$');
            path = hasEndOfStringCharacter ? path.slice(0, -1) : path;
            let normalizedPath = LegacyRouteConverter.tryConvert(path);
            // Fallback to "*path" to support plugins like GraphQL
            normalizedPath = normalizedPath === '/*path' ? '*path' : normalizedPath;
            // Normalize the path to support the prefix if it set in application
            if (this._pathPrefix &&
                !normalizedPath.startsWith(this._pathPrefix) &&
                (normalizedPath === '/' || normalizedPath === '')) {
                normalizedPath = `${this._pathPrefix}${normalizedPath}`;
                if (normalizedPath.endsWith('/')) {
                    normalizedPath = `${normalizedPath}{*path}`;
                }
            }
            try {
                let { regexp: re } = pathToRegexp(normalizedPath);
                re = hasEndOfStringCharacter
                    ? new RegExp(re.source + '$', re.flags)
                    : re;
                // A case-insensitive router lower-cases the decoded path, where some
                // characters only fold to ASCII ("%E2%84%AA", KELVIN SIGN, is routed
                // as "k"). The RegExp of @fastify/middie does not fold them, so on
                // such a router the middleware is registered for every path and the
                // path is only matched below.
                const { caseSensitive } = this.getRouteMatchingOptions();
                const createMiddleware = (stripsPath) => (req, res, next) => {
                    const queryParamsIndex = req.originalUrl.indexOf('?');
                    let pathname = queryParamsIndex >= 0
                        ? req.originalUrl.slice(0, queryParamsIndex)
                        : req.originalUrl;
                    pathname = this.sanitizeUrl(pathname);
                    if (normalizedPath) {
                        const pathToCheck = pathname.endsWith('/')
                            ? pathname
                            : `${pathname}/`;
                        if (!re.exec(pathToCheck)) {
                            return next();
                        }
                        if (stripsPath) {
                            // Strips the path from "req.url", as @fastify/middie does
                            // when it matches the path itself
                            const suffixIndex = req.originalUrl.search(/[?#]/);
                            req.url =
                                suffixIndex >= 0
                                    ? `/${req.originalUrl.slice(suffixIndex)}`
                                    : '/';
                        }
                    }
                    this.runMiddleware(callback, req, res, next);
                };
                // The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie')
                // ref https://github.com/fastify/middie/pull/55
                this.instance.use(caseSensitive ? normalizedPath : '/', createMiddleware(!caseSensitive));
                // The contexts created since middie was registered have a copy of the
                // middleware of their parent without this one (see `registerMiddie()`).
                // It is registered there without its path, which middie would prefix
                // with the one of the context.
                for (const context of this.middieContexts) {
                    context.use(createMiddleware(true));
                }
            }
            catch (e) {
                if (e instanceof TypeError) {
                    LegacyRouteConverter.printError(path);
                }
                throw e;
            }
        };
    }
    runMiddleware(middleware, req, res, next) {
        let completed = false;
        const nextOnce = (err) => {
            if (completed)
                return;
            completed = true;
            next(err);
        };
        try {
            // middie discards the return value, so the promise is observed here:
            // the proxies of `RouterProxy` reject when the exception layer throws.
            // Its resolution is not passed to `next`, as a middleware that answers
            // the request does not call it.
            const result = middleware(req, res, nextOnce);
            if (result && typeof result.then === 'function') {
                result.then(undefined, (err) => {
                    if (completed) {
                        this.logger.error(err);
                        return;
                    }
                    nextOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR());
                });
            }
        }
        catch (err) {
            nextOnce(err);
        }
    }
    getType() {
        return 'fastify';
    }
    isRouteOrderSensitive() {
        return false;
    }
    // The return type is spelled out (rather than "RouteMatchingOptions") so
    // that the declarations still compile against an older "@nestjs/common"
    getRouteMatchingOptions() {
        const initialConfig = this.instance.initialConfig;
        // Absent when a Fastify instance created without it is passed in
        const routerOptions = (initialConfig.routerOptions ??
            {});
        return {
            caseSensitive: !(routerOptions.caseSensitive === false ||
                initialConfig.caseSensitive === false),
            ignoreTrailingSlash: !!(routerOptions.ignoreTrailingSlash || initialConfig.ignoreTrailingSlash),
        };
    }
    use(...args) {
        // Fastify requires @fastify/middie plugin to be registered before middleware can be used.
        // If middie is not registered yet, we queue the middleware and register it later during init.
        if (!this.isMiddieRegistered) {
            this.pendingMiddlewares.push({ args });
            return this;
        }
        return this.instance.use(...args);
    }
    mapException(error) {
        // A 5xx FastifyError (e.g. FST_ERR_HANDLER_TIMEOUT) is a server fault: it
        // stays unmapped, so the built-in filter logs it, like an http-errors one.
        if (this.isHttpFastifyError(error) && error.statusCode < 500) {
            return new HttpException(error.message, error.statusCode, {
                cause: error,
            });
        }
        // http-errors (used by Express middleware such as body-parser) carry a
        // numeric statusCode and an expose flag. Without this case, errors like
        // PayloadTooLargeError reach custom exception filters as a raw http-errors
        // instance rather than an HttpException, as on Express (#18213). A 5xx one
        // is a server fault: it stays unmapped, so the built-in filter still logs it.
        if (isHttpError(error) && error.statusCode < 500) {
            return new HttpException(
            // An error that must not reach the client only gets a status text,
            // like the built-in exception filter answers it
            error.expose === false
                ? (http.STATUS_CODES[error.statusCode] ?? 'Internal server error')
                : error.message, error.statusCode, { cause: error });
        }
        return error;
    }
    isHttpFastifyError(error) {
        // condition based on this code - https://github.com/fastify/fastify-error/blob/d669b150a82968322f9f7be992b2f6b463272de3/index.js#L22
        return (error.statusCode !== undefined &&
            error instanceof Error &&
            error.name === 'FastifyError');
    }
    registerWithPrefix(factory, prefix = '/') {
        return this.instance.register(factory, { prefix });
    }
    isNativeResponse(response) {
        return !('status' in response);
    }
    registerJsonContentParser(rawBody) {
        const contentType = 'application/json';
        if (this.hasCustomBodyParser(contentType)) {
            return;
        }
        const withRawBody = !!rawBody;
        const { bodyLimit } = this.getInstance().initialConfig;
        this.useBodyParser(contentType, withRawBody, { bodyLimit });
    }
    registerUrlencodedContentParser(rawBody) {
        const contentType = 'application/x-www-form-urlencoded';
        if (this.hasCustomBodyParser(contentType)) {
            return;
        }
        const withRawBody = !!rawBody;
        const { bodyLimit } = this.getInstance().initialConfig;
        this.useBodyParser(contentType, withRawBody, { bodyLimit });
    }
    // A parser registered through `useBodyParser()` takes precedence over the
    // default one, including a RegExp or catch-all ('*') parser matching the type.
    hasCustomBodyParser(contentType) {
        for (const type of this.registeredContentTypes) {
            if (type === '*' || type === contentType) {
                return true;
            }
            if (isString(type)) {
                continue;
            }
            type.lastIndex = 0;
            if (type.test(contentType)) {
                return true;
            }
        }
        return false;
    }
    // Fastify stores string content types trimmed and lower-cased.
    normalizeContentType(type) {
        return isString(type) ? type.trim().toLowerCase() : type;
    }
    getDefaultBodyParser(type) {
        switch (this.normalizeContentType(type)) {
            case 'application/json':
                return (req, body, done) => {
                    const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
                    const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
                    defaultJsonParser(req, body, done);
                };
            case 'application/x-www-form-urlencoded':
                return (_req, body, done) => {
                    done(null, querystringParse(body.toString()));
                };
        }
    }
    async registerMiddie() {
        this.isMiddieRegistered = true;
        await this.register(middie);
        // middie gives each encapsulated context created from now on a copy of
        // the middleware of its parent, as it is at that time. As
        // `NestFactory.create()` registers middie before `main.ts` runs, a
        // plugin registered there with an awaited `register()` gets a context
        // without the Nest middleware `app.init()` registers later, so
        // `createMiddlewareFactory()` adds it to the contexts tracked here.
        this.instance.addHook('onRegister', instance => {
            this.middieContexts.push(instance);
        });
    }
    getRequestOriginalUrl(rawRequest) {
        return rawRequest.originalUrl || rawRequest.url;
    }
    injectRouteOptions(routerMethodKey, ...args) {
        const handlerRef = args[args.length - 1];
        const isVersioned = !isUndefined(handlerRef.version) &&
            handlerRef.version !== VERSION_NEUTRAL;
        const routeConfig = Reflect.getMetadata(FASTIFY_ROUTE_CONFIG_METADATA, handlerRef);
        const routeConstraints = Reflect.getMetadata(FASTIFY_ROUTE_CONSTRAINTS_METADATA, handlerRef);
        const routeSchema = Reflect.getMetadata(FASTIFY_ROUTE_SCHEMA_METADATA, handlerRef);
        const hasConfig = !isUndefined(routeConfig);
        const hasConstraints = !isUndefined(routeConstraints);
        const hasSchema = !isUndefined(routeSchema);
        const routeToInject = {
            method: routerMethodKey,
            url: args[0],
            handler: this.requireNonEmptyParams(args[0], handlerRef),
        };
        if (isString(routerMethodKey) &&
            !this.instance.supportedMethods.includes(routerMethodKey)) {
            this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
        }
        if (isVersioned || hasConstraints || hasConfig || hasSchema) {
            const isPathAndRouteTuple = args.length === 2;
            if (isPathAndRouteTuple) {
                const constraints = {
                    ...(hasConstraints && routeConstraints),
                    ...(isVersioned && {
                        version: handlerRef.version,
                    }),
                };
                const options = {
                    constraints,
                    ...(hasConfig && {
                        config: {
                            ...routeConfig,
                        },
                    }),
                    ...(hasSchema && {
                        schema: routeSchema,
                    }),
                };
                const routeToInjectWithOptions = { ...routeToInject, ...options };
                return this.instance.route(routeToInjectWithOptions);
            }
        }
        return this.instance.route(routeToInject);
    }
    /**
     * "find-my-way" matches an empty path segment for a route parameter
     * ("/users//profile" -> { id: "" }), while "path-to-regexp", which
     * middleware paths (and the ExpressAdapter's routes) are matched with,
     * needs at least one character per parameter. Requests with an empty route
     * parameter are passed to the not-found handler instead, as with the
     * ExpressAdapter, so that middleware and routes always match the same
     * requests. Only the wildcard ("*") may be empty.
     */
    requireNonEmptyParams(url, handler) {
        if (isString(url) && !url.includes(':')) {
            return handler;
        }
        const routeHandler = function (request, reply) {
            const params = request.params;
            for (const name in params) {
                if (params[name] === '' && name !== '*') {
                    reply.callNotFound();
                    return;
                }
            }
            return handler.call(this, request, reply);
        };
        Object.defineProperty(routeHandler, 'name', {
            configurable: true,
            value: handler.name,
        });
        return routeHandler;
    }
    /**
     * Fastify still accepts the router options ("ignoreTrailingSlash",
     * "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
     * only reflects them when they are passed through "routerOptions". As the
     * adapter always passes "routerOptions" (for the version constraint), the
     * top-level values are folded in so that plugins relying on
     * "initialConfig.routerOptions" (like @fastify/middie) normalize request
     * paths exactly like the router does.
     */
    getTopLevelRouterOptions(options) {
        const routerOptions = {};
        const routerOptionKeys = [
            'ignoreTrailingSlash',
            'ignoreDuplicateSlashes',
            'caseSensitive',
            'useSemicolonDelimiter',
            'maxParamLength',
            'allowUnsafeRegex',
        ];
        for (const key of routerOptionKeys) {
            if (options?.[key] !== undefined) {
                routerOptions[key] = options[key];
            }
        }
        return routerOptions;
    }
    sanitizeUrl(url) {
        const initialConfig = this.instance.initialConfig;
        const routerOptions = (initialConfig.routerOptions ??
            {});
        // Absolute-form request targets ("GET http://host/path HTTP/1.1") must be
        // resolved to their path before any other normalization, as the Fastify
        // router does, so that middleware and routes always match the same path.
        url = this.getPathFromRequestTarget(url);
        if (routerOptions.ignoreDuplicateSlashes ||
            initialConfig.ignoreDuplicateSlashes) {
            url = this.removeDuplicateSlashes(url);
        }
        const { caseSensitive, ignoreTrailingSlash } = this.getRouteMatchingOptions();
        if (ignoreTrailingSlash) {
            url = this.trimLastSlash(url);
        }
        const { path } = safeDecodeURI(url, routerOptions.useSemicolonDelimiter ||
            initialConfig.useSemicolonDelimiter);
        // Lower-cased once decoded, as the router does: "%E2%84%AA" (KELVIN
        // SIGN) only lower-cases to "k" then
        return caseSensitive ? path : path.toLowerCase();
    }
    removeDuplicateSlashes(path) {
        const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
        return path.indexOf('//') !== -1
            ? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
            : path;
    }
    trimLastSlash(path) {
        if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
            return path.slice(0, -1);
        }
        return path;
    }
    /**
     * Mirrors the absolute-form request target handling of "find-my-way".
     * Returns the path of an absolute-form target ("http://host/path" -> "/path",
     * "http://host?a=b" -> "/?a=b") and leaves any other request target
     * untouched.
     */
    getPathFromRequestTarget(url) {
        if (url.charCodeAt(0) === 47 /* '/' */) {
            return url;
        }
        const schemeEnd = url.indexOf('://');
        if (schemeEnd === -1) {
            return url;
        }
        const scheme = url.slice(0, schemeEnd).toLowerCase();
        if (scheme !== 'http' && scheme !== 'https') {
            return url;
        }
        const authorityStart = schemeEnd + 3;
        const pathStart = url.indexOf('/', authorityStart);
        if (pathStart === authorityStart || !URL.canParse(url)) {
            // Malformed target: the router rejects it before any middleware runs
            return url;
        }
        const queryStart = url.indexOf('?', authorityStart);
        if (queryStart !== -1 && (pathStart === -1 || queryStart < pathStart)) {
            // The query ends the authority, so a "/" in it does not start the path
            // ("http://host?a/users" -> "/?a/users"). "find-my-way" < 9.9.0, which
            // the router of a Fastify instance passed to the adapter may still be,
            // routes such a target as "/users". It is left untouched, so that it
            // is answered by the not-found handler (see `isRoutableRequestTarget()`).
            return pathStart === -1 ? `/${url.slice(queryStart)}` : url;
        }
        return pathStart === -1 ? '/' : url.slice(pathStart);
    }
    /**
     * Whether the request target is in origin-form ("/users") or in the
     * absolute-form of an "http(s)" URL ("http://host/users"), the only forms
     * middleware paths can match.
     *
     * Node.js accepts other targets too (e.g. "GET *users HTTP/1.1").
     * "find-my-way" routes those as if their first character were the leading
     * "/" ("*users" is served by the "/users" route), while no middleware path
     * matches them, so the route would run without its middleware. They are
     * answered by the not-found handler instead, as with the ExpressAdapter,
     * whose router never matches a path that does not start with "/".
     *
     * That includes the asterisk-form "OPTIONS *" (RFC 9112, section 3.2.4),
     * which asks about the server as a whole rather than about a resource:
     * Nest has no handler for it, and "find-my-way" would serve it with the
     * "OPTIONS /" route.
     */
    isRoutableRequestTarget(url) {
        return !!url && this.getPathFromRequestTarget(url).charCodeAt(0) === 47;
    }
    trackOpenConnections() {
        const track = (socket) => {
            if (this.isClosing) {
                // Fastify runs its `preClose` hooks before it stops accepting
                // connections, so destroy anything that arrives in the meantime
                socket.destroy();
                return;
            }
            if (this.openConnections.has(socket)) {
                return;
            }
            this.openConnections.add(socket);
            socket.on('close', () => this.openConnections.delete(socket));
        };
        this.httpServer.on('connection', track);
        // Sockets accepted by the secondary servers Fastify binds for every
        // address `listen()` resolves to are only reachable through requests.
        // `inject()` requests carry a mock socket, which must not be tracked.
        this.instance.addHook('onRequest', (request, _reply, done) => {
            const socket = request.raw.socket;
            if (socket instanceof net.Socket) {
                track(socket);
            }
            done();
        });
    }
    closeOpenConnections() {
        for (const socket of this.openConnections) {
            socket.destroy();
            this.openConnections.delete(socket);
        }
    }
}
/**
 * Returns true when `err` is an "http-errors"-shaped error (body-parser, Express, etc.).
 * Mirrors the same check in BaseExceptionFilter.isHttpError().
 */
function isHttpError(err) {
    return (isObject(err) &&
        err instanceof Error &&
        typeof err.expose === 'boolean' &&
        typeof err.statusCode === 'number' &&
        err.status === err.statusCode);
}