@thi.ng/server
Version:
Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors
241 lines • 8.08 kB
TypeScript
import type { Fn, Fn2, Maybe, MaybePromise } from "@thi.ng/api";
import type { ILogger } from "@thi.ng/logger";
import type { Route, RouteMatch } from "@thi.ng/router";
import type { IncomingMessage } from "node:http";
import type { BlockList } from "node:net";
import type { Server, ServerResponse } from "./server.js";
export type Method = "get" | "put" | "post" | "delete" | "head" | "options" | "patch";
export interface ServerOpts<CTX extends RequestCtx = RequestCtx> {
logger: ILogger;
/**
* SSL configuration
*/
ssl: {
key: string;
cert: string;
};
/**
* Host name this server should respond to.
*
* @defaultValue `localhost`
*/
host: string;
/**
* Port number.
*
* @defaultValue 443 / 8080
*/
port: number;
/**
* Initial list of routes (more can be added dynamically via
* {@link Server.addRoutes}).
*/
routes: ServerRoute<CTX>[];
/**
* Route prefix. Default: `/`. All routes are assumed to have this prefix
* prepended. If given, the prefix MUST end with `/`.
*/
prefix: string;
/**
* If true (default), the trailing slash of a given route input string will
* be removed before matching.
*/
trim: boolean;
/**
* Interceptors (aka pre/post middlewares) which are to be applied to all
* route handlers (in the given order).
*/
intercept: Interceptor<CTX>[];
/**
* User defined function to augment the {@link RequestCtx} object for each
* request before processing its handler & interceptors.
*/
context: Fn<RequestCtx, CTX>;
/**
* Timeout in milliseconds for receiving the entire request from the client.
*/
requestTimeout: number;
/**
* List of response headers that should be sent only once.
*/
uniqueHeaders: string[];
/**
* Used for disabling inbound access to specific IP addresses, IP ranges, or
* IP subnets. This does not work if the server is behind a reverse proxy.
*
* @remarks
* Reference: https://nodejs.org/api/net.html#class-netblocklist
*/
blockList: BlockList;
/**
* HTTP method adapter/converter. The default implementation only converts
* {@link Method} `head` to `get` if the route does not provide a `head`
* handler.
*/
method: Fn2<Method, Pick<RequestCtx, "req" | "route" | "match" | "cookies" | "query">, Method>;
}
export interface ServerRoute<CTX extends RequestCtx = RequestCtx> extends Route {
handlers: Partial<Record<Method, RequestHandler<CTX>>>;
}
/**
* Version of {@link ServerRoute} whose handlers/interceptors already have been
* pre-processed.
*/
export interface CompiledServerRoute<CTX extends RequestCtx = RequestCtx> extends Route {
handlers: Partial<Record<Method, CompiledHandler<CTX>>>;
}
export interface RequestCtx {
/**
* Server instance.
*/
server: Server;
/**
* Logger instance.
*/
logger: ILogger;
/**
* Parsed request URL.
*
* @remarks
* Route handlers SHOULD only use this url instead of
* {@link RequestCtx.req}'s `.url` property. This ensures, that the
* hash-fragment (`#...`) part is properly dealt with, avoiding security
* issues related to HTTP `request-target`
* (https://datatracker.ietf.org/doc/html/rfc9112#section-3.2).
*
* Note: The server already responds to any request containing a `#` in its
* URL with a HTTP 400.
*
* Also see {@link RequestCtx.path} and {@link RequestCtx.query}.
*/
url: URL;
/**
* HTTP request instance.
*/
req: IncomingMessage;
/**
* HTTP server response instance.
*/
res: ServerResponse;
/**
* Pre-compiled route handler and its interceptors
*/
route: CompiledServerRoute;
/**
* Parsed route details.
*/
match: RouteMatch;
/**
* Request method (possibly adapted). Also see {@link RequestCtx.origMethod}.
*/
method: Method;
/**
* Original request method. Also see {@link RequestCtx.method}.
*/
origMethod: Method;
/**
* URI-decoded path part of request URL.
*/
path: string;
/**
* Parsed query string params (aka URL search params).
*/
query: Record<string, any>;
/**
* Parsed cookies, if any.
*/
cookies?: Record<string, string>;
/**
* Server session (only if {@link SessionInterceptor} is used).
*/
session?: ServerSession;
}
export type HandlerResult = MaybePromise<void>;
export type PreInterceptorResult = MaybePromise<boolean>;
export type PostInterceptorResult = MaybePromise<void>;
export type RequestHandler<CTX extends RequestCtx = RequestCtx> = Fn<CTX, HandlerResult> | InterceptedRequestHandler<CTX>;
export interface InterceptedRequestHandler<CTX extends RequestCtx = RequestCtx> {
fn: Fn<CTX, HandlerResult>;
/**
* List of interceptors which will be executed when processing the main
* handler {@link InterceptedRequestHandler.fn}.
*
* @remarks
* Handler-specific interceptors will be appended to the list global
* interceptors (aka {@link ServerOpts.intercept}). Processing then happens
* in this order:
*
* - {@link Interceptor.pre} interceptors in the order given
* - {@link InterceptedRequestHandler.fn} main handler
* - {@link Interceptor.post} interceptors in the REVERSE order given
*
* If an interceptor function returns false, further processing stops and
* response will be closed.
*/
intercept: Interceptor<CTX>[];
}
export interface CompiledHandler<CTX extends RequestCtx = RequestCtx> {
fn: Fn<CTX, HandlerResult>;
pre?: Maybe<Fn<CTX, PreInterceptorResult>>[];
post?: Maybe<Fn<CTX, PostInterceptorResult>>[];
}
export interface Interceptor<CTX extends RequestCtx = RequestCtx> {
/**
* Interceptor function which will be run BEFORE the main route handler (aka
* {@link InterceptedRequestHandler.fn}). If an interceptor needs to cancel
* the request processing it must return `false`. In this case any further
* pre-interceptors and the main handler will be skipped. In the post-phase,
* only the interceptors preceding the failed one will be run (though in
* reverse order). E.g. If the 3rd pre-interceptor failed, only the post
* phases of the first two will still be run (if available)...
*/
pre?: Fn<CTX, PreInterceptorResult>;
/**
* Interceptor function which will be run AFTER the main route handler (aka
* {@link InterceptedRequestHandler.fn}). Post-interceptors cannot cancel
* request processing and are mainly intended for logging or clean-up
* purposes. Post interceptors
*/
post?: Fn<CTX, PostInterceptorResult>;
}
export interface ServerSession {
/**
* Unique session ID
*/
id: string;
/**
* Client's remote IP address when session was originally created. To
* counteract session fixation, each request's remote address is being
* checked (by {@link SessionInterceptor.pre}) against this stored address.
* If there's a mismatch between the two, then a new session will be
* generated automatically.
*/
ip: string;
}
export interface ISessionStore<T extends ServerSession = ServerSession> {
/**
* Attempts to retrieve the session for given `id`.
*
* @param id
*/
get(id: string): MaybePromise<Maybe<T>>;
/**
* Adds to or updates given `session` in underlying storage.
*
* @param session
*/
set(session: T): MaybePromise<boolean>;
/**
* Attempts to delete the session for given `id` from storage. Returns true
* if successful.
*
* @param id
*/
delete(id: string): MaybePromise<boolean>;
/**
* Configured Time-To-Live for stored sessions. Will also be used to
* configure the `max-age` attribute of the session ID cookie.
*/
readonly ttl: number;
}
//# sourceMappingURL=api.d.ts.map