mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
918 lines
26 KiB
TypeScript
918 lines
26 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
import { FastifyCorsOptions } from '@fastify/cors';
|
|
import {
|
|
HttpStatus,
|
|
Logger,
|
|
RawBodyRequest,
|
|
RequestMethod,
|
|
StreamableFile,
|
|
VERSION_NEUTRAL,
|
|
VersioningOptions,
|
|
VersioningType,
|
|
} from '@nestjs/common';
|
|
import { VersionValue } from '@nestjs/common/interfaces';
|
|
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
|
import { isString, isUndefined } from '@nestjs/common/utils/shared.utils';
|
|
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
|
|
import { LegacyRouteConverter } from '@nestjs/core/router/legacy-route-converter';
|
|
import {
|
|
FastifyBaseLogger,
|
|
FastifyBodyParser,
|
|
FastifyInstance,
|
|
FastifyListenOptions,
|
|
FastifyPluginAsync,
|
|
FastifyPluginCallback,
|
|
FastifyRegister,
|
|
FastifyReply,
|
|
FastifyRequest,
|
|
FastifyServerOptions,
|
|
HTTPMethods,
|
|
RawReplyDefaultExpression,
|
|
RawRequestDefaultExpression,
|
|
RawServerBase,
|
|
RawServerDefault,
|
|
RequestGenericInterface,
|
|
RouteGenericInterface,
|
|
RouteOptions,
|
|
RouteShorthandOptions,
|
|
fastify,
|
|
} from 'fastify';
|
|
import * as Reply from 'fastify/lib/reply';
|
|
import { kRouteContext } from 'fastify/lib/symbols';
|
|
import * as http from 'http';
|
|
import * as http2 from 'http2';
|
|
import * as https from 'https';
|
|
import {
|
|
InjectOptions,
|
|
Chain as LightMyRequestChain,
|
|
Response as LightMyRequestResponse,
|
|
} from 'light-my-request';
|
|
import { pathToRegexp } from 'path-to-regexp';
|
|
// Fastify uses `fast-querystring` internally to quickly parse URL query strings.
|
|
import { parse as querystringParse } from 'fast-querystring';
|
|
import { safeDecodeURI } from 'find-my-way/lib/url-sanitizer';
|
|
import {
|
|
FASTIFY_ROUTE_CONFIG_METADATA,
|
|
FASTIFY_ROUTE_CONSTRAINTS_METADATA,
|
|
FASTIFY_ROUTE_SCHEMA_METADATA,
|
|
} from '../constants';
|
|
import { NestFastifyBodyParserOptions } from '../interfaces';
|
|
import {
|
|
FastifyStaticOptions,
|
|
FastifyViewOptions,
|
|
} from '../interfaces/external';
|
|
import middie from './middie/fastify-middie';
|
|
|
|
type FastifyAdapterBaseOptions<
|
|
Server extends RawServerBase = RawServerDefault,
|
|
Logger extends FastifyBaseLogger = FastifyBaseLogger,
|
|
> = FastifyServerOptions<Server, Logger> & {
|
|
skipMiddie?: boolean;
|
|
};
|
|
|
|
type FastifyHttp2SecureOptions<
|
|
Server extends http2.Http2SecureServer,
|
|
Logger extends FastifyBaseLogger = FastifyBaseLogger,
|
|
> = FastifyAdapterBaseOptions<Server, Logger> & {
|
|
http2: true;
|
|
https: http2.SecureServerOptions;
|
|
};
|
|
|
|
type FastifyHttp2Options<
|
|
Server extends http2.Http2Server,
|
|
Logger extends FastifyBaseLogger = FastifyBaseLogger,
|
|
> = FastifyAdapterBaseOptions<Server, Logger> & {
|
|
http2: true;
|
|
http2SessionTimeout?: number;
|
|
};
|
|
|
|
type FastifyHttpsOptions<
|
|
Server extends https.Server,
|
|
Logger extends FastifyBaseLogger = FastifyBaseLogger,
|
|
> = FastifyAdapterBaseOptions<Server, Logger> & {
|
|
https: https.ServerOptions;
|
|
};
|
|
|
|
type FastifyHttpOptions<
|
|
Server extends http.Server,
|
|
Logger extends FastifyBaseLogger = FastifyBaseLogger,
|
|
> = FastifyAdapterBaseOptions<Server, Logger> & {
|
|
http: http.ServerOptions;
|
|
};
|
|
|
|
type VersionedRoute<TRequest, TResponse> = ((
|
|
req: TRequest,
|
|
res: TResponse,
|
|
next: Function,
|
|
) => Function) & {
|
|
version: VersionValue;
|
|
versioningOptions: VersioningOptions;
|
|
};
|
|
|
|
/**
|
|
* The following type assertion is valid as we enforce "middie" plugin registration
|
|
* which enhances the FastifyRequest.RawRequest with the "originalUrl" property.
|
|
* ref https://github.com/fastify/middie/pull/16
|
|
* ref https://github.com/fastify/fastify/pull/559
|
|
*/
|
|
type FastifyRawRequest<TServer extends RawServerBase> =
|
|
RawRequestDefaultExpression<TServer> & { originalUrl?: string };
|
|
|
|
/**
|
|
* @publicApi
|
|
*/
|
|
export class FastifyAdapter<
|
|
TServer extends RawServerBase = RawServerDefault,
|
|
TRawRequest extends FastifyRawRequest<TServer> = FastifyRawRequest<TServer>,
|
|
TRawResponse extends RawReplyDefaultExpression<TServer> =
|
|
RawReplyDefaultExpression<TServer>,
|
|
TRequest extends FastifyRequest<
|
|
RequestGenericInterface,
|
|
TServer,
|
|
TRawRequest
|
|
> = FastifyRequest<RequestGenericInterface, TServer, TRawRequest>,
|
|
TReply extends FastifyReply<
|
|
RouteGenericInterface,
|
|
TServer,
|
|
TRawRequest,
|
|
TRawResponse
|
|
> = FastifyReply<RouteGenericInterface, TServer, TRawRequest, TRawResponse>,
|
|
TInstance extends FastifyInstance<TServer, TRawRequest, TRawResponse> =
|
|
FastifyInstance<TServer, TRawRequest, TRawResponse>,
|
|
> extends AbstractHttpAdapter<TServer, TRequest, TReply> {
|
|
protected readonly logger = new Logger(FastifyAdapter.name);
|
|
protected readonly instance: TInstance;
|
|
protected _pathPrefix?: string;
|
|
|
|
private _isParserRegistered: boolean;
|
|
private onRequestHook?: (
|
|
request: TRequest,
|
|
reply: TReply,
|
|
done: (err?: Error) => void,
|
|
) => void | Promise<void>;
|
|
private onResponseHook?: (
|
|
request: TRequest,
|
|
reply: TReply,
|
|
done: (err?: Error) => void,
|
|
) => void | Promise<void>;
|
|
private isMiddieRegistered: boolean;
|
|
private pendingMiddlewares: Array<{ args: any[] }> = [];
|
|
private versioningOptions?: VersioningOptions;
|
|
private readonly versionConstraint = {
|
|
name: 'version',
|
|
validate(value: unknown) {
|
|
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<string, unknown>();
|
|
return {
|
|
get(version: string | Array<string>) {
|
|
if (Array.isArray(version)) {
|
|
return versions.get(version.find(v => versions.has(v))!) || null;
|
|
}
|
|
return versions.get(version) || null;
|
|
},
|
|
set(versionOrVersions: string | Array<string>, store: unknown) {
|
|
const storeVersionConstraint = (version: string) =>
|
|
versions.set(version, store);
|
|
if (Array.isArray(versionOrVersions))
|
|
versionOrVersions.forEach(storeVersionConstraint);
|
|
else storeVersionConstraint(versionOrVersions);
|
|
},
|
|
del(version: string | Array<string>) {
|
|
if (Array.isArray(version)) {
|
|
version.forEach(v => versions.delete(v));
|
|
} else {
|
|
versions.delete(version);
|
|
}
|
|
},
|
|
empty() {
|
|
versions.clear();
|
|
},
|
|
};
|
|
},
|
|
deriveConstraint: (req: FastifyRequest) => {
|
|
// Media Type (Accept Header) Versioning Handler
|
|
if (this.versioningOptions?.type === VersioningType.MEDIA_TYPE) {
|
|
const MEDIA_TYPE_HEADER = 'Accept';
|
|
const acceptHeaderValue: string | undefined = (req.headers?.[
|
|
MEDIA_TYPE_HEADER
|
|
] || req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()]) as string;
|
|
|
|
const acceptHeaderVersionParameter = acceptHeaderValue
|
|
? acceptHeaderValue.split(';')[1]
|
|
: '';
|
|
|
|
return isUndefined(acceptHeaderVersionParameter)
|
|
? VERSION_NEUTRAL // No version was supplied
|
|
: acceptHeaderVersionParameter.split(this.versioningOptions.key)[1];
|
|
}
|
|
// Header Versioning Handler
|
|
else if (this.versioningOptions?.type === VersioningType.HEADER) {
|
|
const customHeaderVersionParameter: string | string[] | undefined =
|
|
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) {
|
|
return this.versioningOptions.extractor(req);
|
|
}
|
|
return undefined;
|
|
},
|
|
mustMatchWhenDerived: false,
|
|
};
|
|
|
|
get isParserRegistered(): boolean {
|
|
return !!this._isParserRegistered;
|
|
}
|
|
|
|
constructor(
|
|
instanceOrOptions?:
|
|
| TInstance
|
|
| FastifyHttp2Options<any>
|
|
| FastifyHttp2SecureOptions<any>
|
|
| FastifyHttpsOptions<any>
|
|
| FastifyHttpOptions<any>
|
|
| FastifyAdapterBaseOptions<TServer>,
|
|
) {
|
|
super();
|
|
|
|
const instance =
|
|
instanceOrOptions && (instanceOrOptions as TInstance).server
|
|
? instanceOrOptions
|
|
: fastify({
|
|
...(instanceOrOptions as FastifyServerOptions),
|
|
routerOptions: {
|
|
...(instanceOrOptions as FastifyServerOptions)?.routerOptions,
|
|
constraints: {
|
|
version: this.versionConstraint as any,
|
|
},
|
|
},
|
|
});
|
|
|
|
this.setInstance(instance);
|
|
|
|
if ((instanceOrOptions as FastifyAdapterBaseOptions)?.skipMiddie) {
|
|
this.isMiddieRegistered = true;
|
|
}
|
|
|
|
this.instance.addHook('onRequest', (request, reply, done) => {
|
|
if (this.onRequestHook) {
|
|
this.onRequestHook(request as TRequest, reply as TReply, done);
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
|
|
this.instance.addHook('onResponse', (request, reply, done) => {
|
|
if (this.onResponseHook) {
|
|
this.onResponseHook(request as TRequest, reply as TReply, done);
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
}
|
|
|
|
public setOnRequestHook(
|
|
hook: (
|
|
request: TRequest,
|
|
reply: TReply,
|
|
done: (err?: Error) => void,
|
|
) => void | Promise<void>,
|
|
) {
|
|
this.onRequestHook = hook;
|
|
}
|
|
|
|
public setOnResponseHook(
|
|
hook: (
|
|
request: TRequest,
|
|
reply: TReply,
|
|
done: (err?: Error) => void,
|
|
) => void | Promise<void>,
|
|
) {
|
|
this.onResponseHook = hook;
|
|
}
|
|
|
|
public async init() {
|
|
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 as any)(...args);
|
|
}
|
|
this.pendingMiddlewares = [];
|
|
}
|
|
}
|
|
|
|
public listen(port: string | number, callback?: () => void): void;
|
|
public listen(
|
|
port: string | number,
|
|
hostname: string,
|
|
callback?: () => void,
|
|
): void;
|
|
public listen(
|
|
listenOptions: string | number | FastifyListenOptions,
|
|
...args: any[]
|
|
): void {
|
|
const isFirstArgTypeofFunction = typeof args[0] === 'function';
|
|
const callback = isFirstArgTypeofFunction ? args[0] : args[1];
|
|
|
|
let options: Record<string, any>;
|
|
if (
|
|
typeof listenOptions === 'object' &&
|
|
(listenOptions.host !== undefined ||
|
|
listenOptions.port !== undefined ||
|
|
listenOptions.path !== undefined)
|
|
) {
|
|
// First parameter is an object with a path, port and/or host attributes
|
|
options = listenOptions;
|
|
} else {
|
|
options = {
|
|
port: +listenOptions,
|
|
};
|
|
}
|
|
if (!isFirstArgTypeofFunction) {
|
|
options.host = args[0];
|
|
}
|
|
return this.instance.listen(options, callback);
|
|
}
|
|
|
|
public get(...args: any[]) {
|
|
return this.injectRouteOptions('GET', ...args);
|
|
}
|
|
|
|
public post(...args: any[]) {
|
|
return this.injectRouteOptions('POST', ...args);
|
|
}
|
|
|
|
public head(...args: any[]) {
|
|
return this.injectRouteOptions('HEAD', ...args);
|
|
}
|
|
|
|
public delete(...args: any[]) {
|
|
return this.injectRouteOptions('DELETE', ...args);
|
|
}
|
|
|
|
public put(...args: any[]) {
|
|
return this.injectRouteOptions('PUT', ...args);
|
|
}
|
|
|
|
public patch(...args: any[]) {
|
|
return this.injectRouteOptions('PATCH', ...args);
|
|
}
|
|
|
|
public options(...args: any[]) {
|
|
return this.injectRouteOptions('OPTIONS', ...args);
|
|
}
|
|
|
|
public search(...args: any[]) {
|
|
return this.injectRouteOptions('SEARCH', ...args);
|
|
}
|
|
|
|
public propfind(...args: any[]) {
|
|
return this.injectRouteOptions('PROPFIND', ...args);
|
|
}
|
|
|
|
public proppatch(...args: any[]) {
|
|
return this.injectRouteOptions('PROPPATCH', ...args);
|
|
}
|
|
|
|
public mkcol(...args: any[]) {
|
|
return this.injectRouteOptions('MKCOL', ...args);
|
|
}
|
|
|
|
public copy(...args: any[]) {
|
|
return this.injectRouteOptions('COPY', ...args);
|
|
}
|
|
|
|
public move(...args: any[]) {
|
|
return this.injectRouteOptions('MOVE', ...args);
|
|
}
|
|
|
|
public lock(...args: any[]) {
|
|
return this.injectRouteOptions('LOCK', ...args);
|
|
}
|
|
|
|
public unlock(...args: any[]) {
|
|
return this.injectRouteOptions('UNLOCK', ...args);
|
|
}
|
|
|
|
public applyVersionFilter(
|
|
handler: Function,
|
|
version: VersionValue,
|
|
versioningOptions: VersioningOptions,
|
|
): VersionedRoute<TRequest, TReply> {
|
|
if (!this.versioningOptions) {
|
|
this.versioningOptions = versioningOptions;
|
|
}
|
|
const versionedRoute = handler as VersionedRoute<TRequest, TReply>;
|
|
versionedRoute.version = version;
|
|
return versionedRoute;
|
|
}
|
|
|
|
public reply(
|
|
response: TRawResponse | TReply,
|
|
body: any,
|
|
statusCode?: number,
|
|
) {
|
|
const fastifyReply: TReply = this.isNativeResponse(response)
|
|
? new Reply(
|
|
response,
|
|
{
|
|
[kRouteContext]: {
|
|
preSerialization: null,
|
|
preValidation: [],
|
|
preHandler: [],
|
|
onSend: [],
|
|
onError: [],
|
|
},
|
|
},
|
|
{},
|
|
)
|
|
: response;
|
|
|
|
if (statusCode) {
|
|
fastifyReply.status(statusCode);
|
|
}
|
|
if (body instanceof StreamableFile) {
|
|
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);
|
|
}
|
|
if (
|
|
fastifyReply.getHeader('Content-Length') === undefined &&
|
|
streamHeaders.length !== undefined
|
|
) {
|
|
fastifyReply.header('Content-Length', streamHeaders.length);
|
|
}
|
|
body = body.getStream();
|
|
}
|
|
if (
|
|
fastifyReply.getHeader('Content-Type') !== undefined &&
|
|
fastifyReply.getHeader('Content-Type') !== 'application/json' &&
|
|
body?.statusCode >= HttpStatus.BAD_REQUEST
|
|
) {
|
|
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);
|
|
}
|
|
|
|
public status(response: TRawResponse | TReply, statusCode: number) {
|
|
if (this.isNativeResponse(response)) {
|
|
response.statusCode = statusCode;
|
|
return response;
|
|
}
|
|
return (response as { code: Function }).code(statusCode);
|
|
}
|
|
|
|
public end(response: TReply, message?: string) {
|
|
response.raw.end(message!);
|
|
}
|
|
|
|
public render(
|
|
response: TReply & { view: Function },
|
|
view: string,
|
|
options: any,
|
|
) {
|
|
return response && response.view(view, options);
|
|
}
|
|
|
|
public redirect(response: TReply, statusCode: number, url: string) {
|
|
const code = statusCode ?? HttpStatus.FOUND;
|
|
return response.status(code).redirect(url);
|
|
}
|
|
|
|
public setErrorHandler(handler: Parameters<TInstance['setErrorHandler']>[0]) {
|
|
return this.instance.setErrorHandler(handler);
|
|
}
|
|
|
|
public setNotFoundHandler(handler: Function) {
|
|
return this.instance.setNotFoundHandler(handler as any);
|
|
}
|
|
|
|
public getHttpServer<T = TServer>(): T {
|
|
return this.instance.server as unknown as T;
|
|
}
|
|
|
|
public getInstance<T = TInstance>(): T {
|
|
return this.instance as unknown as T;
|
|
}
|
|
|
|
public register<
|
|
TRegister extends Parameters<
|
|
FastifyRegister<FastifyInstance<TServer, TRawRequest, TRawResponse>>
|
|
>,
|
|
>(plugin: TRegister['0'], opts?: TRegister['1']) {
|
|
return (this.instance.register as any)(plugin, opts);
|
|
}
|
|
|
|
public inject(): LightMyRequestChain;
|
|
public inject(opts: InjectOptions | string): Promise<LightMyRequestResponse>;
|
|
public inject(
|
|
opts?: InjectOptions | string,
|
|
): LightMyRequestChain | Promise<LightMyRequestResponse> {
|
|
return this.instance.inject(opts!);
|
|
}
|
|
|
|
public async close() {
|
|
try {
|
|
return await this.instance.close();
|
|
} catch (err) {
|
|
// Check if server is still running
|
|
if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
throw err;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
public initHttpServer() {
|
|
this.httpServer = this.instance.server;
|
|
}
|
|
|
|
public useStaticAssets(options: FastifyStaticOptions) {
|
|
return this.register(
|
|
loadPackage('@fastify/static', 'FastifyAdapter.useStaticAssets()', () =>
|
|
require('@fastify/static'),
|
|
),
|
|
options,
|
|
);
|
|
}
|
|
|
|
public setViewEngine(options: FastifyViewOptions | string) {
|
|
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()', () =>
|
|
require('@fastify/view'),
|
|
),
|
|
options,
|
|
);
|
|
}
|
|
|
|
public isHeadersSent(response: TReply): boolean {
|
|
return response.sent;
|
|
}
|
|
|
|
public getHeader(response: any, name: string) {
|
|
return response.getHeader(name);
|
|
}
|
|
|
|
public setHeader(response: TReply, name: string, value: string) {
|
|
return response.header(name, value);
|
|
}
|
|
|
|
public appendHeader(response: any, name: string, value: string) {
|
|
response.header(name, value);
|
|
}
|
|
|
|
public getRequestHostname(request: TRequest): string {
|
|
return request.hostname;
|
|
}
|
|
|
|
public getRequestMethod(request: TRequest): string {
|
|
return request.raw ? request.raw.method! : request.method;
|
|
}
|
|
|
|
public getRequestUrl(request: TRequest): string;
|
|
public getRequestUrl(request: TRawRequest): string;
|
|
public getRequestUrl(request: TRequest & TRawRequest): string {
|
|
return this.getRequestOriginalUrl(request.raw || request);
|
|
}
|
|
|
|
public enableCors(options?: FastifyCorsOptions) {
|
|
this.register(
|
|
import('@fastify/cors') as Parameters<TInstance['register']>[0],
|
|
options,
|
|
);
|
|
}
|
|
|
|
public registerParserMiddleware(prefix?: string, rawBody?: boolean) {
|
|
if (this._isParserRegistered) {
|
|
return;
|
|
}
|
|
|
|
this.registerUrlencodedContentParser(rawBody);
|
|
this.registerJsonContentParser(rawBody);
|
|
|
|
this._isParserRegistered = true;
|
|
this._pathPrefix = prefix
|
|
? !prefix.startsWith('/')
|
|
? `/${prefix}`
|
|
: prefix
|
|
: undefined;
|
|
}
|
|
|
|
public useBodyParser(
|
|
type: string | string[] | RegExp,
|
|
rawBody: boolean,
|
|
options?: NestFastifyBodyParserOptions,
|
|
parser?: FastifyBodyParser<Buffer, TServer>,
|
|
) {
|
|
const parserOptions = {
|
|
...(options || {}),
|
|
parseAs: 'buffer' as const,
|
|
};
|
|
|
|
this.getInstance().addContentTypeParser<Buffer>(
|
|
type,
|
|
parserOptions,
|
|
(
|
|
req: RawBodyRequest<FastifyRequest<any, TServer, TRawRequest>>,
|
|
body: Buffer,
|
|
done,
|
|
) => {
|
|
if (rawBody === true && Buffer.isBuffer(body)) {
|
|
req.rawBody = body;
|
|
}
|
|
|
|
if (parser) {
|
|
parser(req, body, done);
|
|
return;
|
|
}
|
|
|
|
done(null, body);
|
|
},
|
|
);
|
|
|
|
// To avoid the Nest application init to override our custom
|
|
// body parser, we mark the parsers as registered.
|
|
this._isParserRegistered = true;
|
|
}
|
|
|
|
public async createMiddlewareFactory(
|
|
requestMethod: RequestMethod,
|
|
): Promise<(path: string, callback: Function) => any> {
|
|
if (!this.isMiddieRegistered) {
|
|
await this.registerMiddie();
|
|
}
|
|
return (path: string, callback: Function) => {
|
|
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 = `${this._pathPrefix}${normalizedPath}`;
|
|
if (normalizedPath.endsWith('/')) {
|
|
normalizedPath = `${normalizedPath}{*path}`;
|
|
}
|
|
}
|
|
|
|
try {
|
|
let { regexp: re } = pathToRegexp(normalizedPath);
|
|
re = hasEndOfStringCharacter
|
|
? new RegExp(re.source + '$', re.flags)
|
|
: re;
|
|
|
|
// 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(
|
|
normalizedPath,
|
|
(req: any, res: any, next: Function) => {
|
|
const queryParamsIndex = req.originalUrl.indexOf('?');
|
|
let pathname =
|
|
queryParamsIndex >= 0
|
|
? req.originalUrl.slice(0, queryParamsIndex)
|
|
: req.originalUrl;
|
|
|
|
pathname = this.sanitizeUrl(pathname);
|
|
|
|
if (!re.exec(pathname + '/') && normalizedPath) {
|
|
return next();
|
|
}
|
|
return callback(req, res, next);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
LegacyRouteConverter.printError(path);
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
}
|
|
|
|
public getType(): string {
|
|
return 'fastify';
|
|
}
|
|
|
|
public use(...args: any[]) {
|
|
// 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 as any)(...args);
|
|
}
|
|
|
|
protected registerWithPrefix(
|
|
factory:
|
|
| FastifyPluginCallback<any>
|
|
| FastifyPluginAsync<any>
|
|
| Promise<{ default: FastifyPluginCallback<any> }>
|
|
| Promise<{ default: FastifyPluginAsync<any> }>,
|
|
prefix = '/',
|
|
) {
|
|
return this.instance.register(factory, { prefix });
|
|
}
|
|
|
|
private isNativeResponse(
|
|
response: TRawResponse | TReply,
|
|
): response is TRawResponse {
|
|
return !('status' in response);
|
|
}
|
|
|
|
private registerJsonContentParser(rawBody?: boolean) {
|
|
const contentType = 'application/json';
|
|
const withRawBody = !!rawBody;
|
|
const { bodyLimit } = this.getInstance().initialConfig;
|
|
|
|
this.useBodyParser(
|
|
contentType,
|
|
withRawBody,
|
|
{ bodyLimit },
|
|
(req, body, done) => {
|
|
const { onProtoPoisoning, onConstructorPoisoning } =
|
|
this.instance.initialConfig;
|
|
const defaultJsonParser = this.instance.getDefaultJsonParser(
|
|
onProtoPoisoning || 'error',
|
|
onConstructorPoisoning || 'error',
|
|
) as FastifyBodyParser<string | Buffer, TServer>;
|
|
defaultJsonParser(req, body, done);
|
|
},
|
|
);
|
|
}
|
|
|
|
private registerUrlencodedContentParser(rawBody?: boolean) {
|
|
const contentType = 'application/x-www-form-urlencoded';
|
|
const withRawBody = !!rawBody;
|
|
const { bodyLimit } = this.getInstance().initialConfig;
|
|
|
|
this.useBodyParser(
|
|
contentType,
|
|
withRawBody,
|
|
{ bodyLimit },
|
|
(_req, body, done) => {
|
|
done(null, querystringParse(body.toString()));
|
|
},
|
|
);
|
|
}
|
|
|
|
private async registerMiddie() {
|
|
this.isMiddieRegistered = true;
|
|
await this.register(middie as Parameters<TInstance['register']>[0]);
|
|
}
|
|
|
|
private getRequestOriginalUrl(rawRequest: TRawRequest) {
|
|
return rawRequest.originalUrl || rawRequest.url!;
|
|
}
|
|
|
|
private injectRouteOptions(
|
|
routerMethodKey: Uppercase<HTTPMethods>,
|
|
...args: any[]
|
|
) {
|
|
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: RouteOptions<TServer, TRawRequest, TRawResponse> &
|
|
RouteShorthandOptions = {
|
|
method: routerMethodKey,
|
|
url: args[0],
|
|
handler: handlerRef,
|
|
};
|
|
|
|
if (this.instance.supportedMethods.indexOf(routerMethodKey) === -1) {
|
|
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);
|
|
}
|
|
|
|
private sanitizeUrl(url: string): string {
|
|
const initialConfig = this.instance.initialConfig as FastifyServerOptions;
|
|
const routerOptions =
|
|
initialConfig.routerOptions as Partial<FastifyServerOptions>;
|
|
|
|
if (
|
|
routerOptions.ignoreDuplicateSlashes ||
|
|
initialConfig.ignoreDuplicateSlashes
|
|
) {
|
|
url = this.removeDuplicateSlashes(url);
|
|
}
|
|
|
|
if (
|
|
routerOptions.ignoreTrailingSlash ||
|
|
initialConfig.ignoreTrailingSlash
|
|
) {
|
|
url = this.trimLastSlash(url);
|
|
}
|
|
|
|
if (
|
|
routerOptions.caseSensitive === false ||
|
|
initialConfig.caseSensitive === false
|
|
) {
|
|
url = url.toLowerCase();
|
|
}
|
|
return safeDecodeURI(
|
|
url,
|
|
routerOptions.useSemicolonDelimiter ||
|
|
initialConfig.useSemicolonDelimiter,
|
|
).path;
|
|
}
|
|
|
|
private removeDuplicateSlashes(path: string) {
|
|
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
|
|
return path.indexOf('//') !== -1
|
|
? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
|
|
: path;
|
|
}
|
|
|
|
private trimLastSlash(path: string) {
|
|
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
|
|
return path.slice(0, -1);
|
|
}
|
|
return path;
|
|
}
|
|
}
|