/** * @module * This module is the base module for the Hono object. */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { compose } from './compose' import { Context } from './context' import type { ExecutionContext } from './context' import type { Router } from './router' import { METHODS, METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE } from './router' import type { Env, ErrorHandler, FetchEventLike, H, HTTPResponseError, HandlerInterface, MergePath, MergeSchemaPath, MiddlewareHandler, MiddlewareHandlerInterface, Next, NotFoundHandler, OnHandlerInterface, RouterRoute, Schema, } from './types' import { getPath, getPathNoStrict, mergePath } from './utils/url' /** * Symbol used to mark a composed handler. */ export const COMPOSED_HANDLER = Symbol('composedHandler') const notFoundHandler = (c: Context) => { return c.text('404 Not Found', 404) } const errorHandler = (err: Error | HTTPResponseError, c: Context) => { if ('getResponse' in err) { return err.getResponse() } console.error(err) return c.text('Internal Server Error', 500) } type GetPath = (request: Request, options?: { env?: E['Bindings'] }) => string export type HonoOptions = { /** * `strict` option specifies whether to distinguish whether the last path is a directory or not. * * @see {@link https://hono.dev/docs/api/hono#strict-mode} * * @default true */ strict?: boolean /** * `router` option specifices which router to use. * * @see {@link https://hono.dev/docs/api/hono#router-option} * * @example * ```ts * const app = new Hono({ router: new RegExpRouter() }) * ``` */ router?: Router<[H, RouterRoute]> /** * `getPath` can handle the host header value. * * @see {@link https://hono.dev/docs/api/routing#routing-with-host-header-value} * * @example * ```ts * const app = new Hono({ * getPath: (req) => * '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/[^?]*)/, '$1'), * }) * * app.get('/www1.example.com/hello', () => c.text('hello www1')) * * // A following request will match the route: * // new Request('http://www1.example.com/hello', { * // headers: { host: 'www1.example.com' }, * // }) * ``` */ getPath?: GetPath } type MountOptionHandler = (c: Context) => unknown type MountReplaceRequest = (originalRequest: Request) => Request type MountOptions = | MountOptionHandler | { optionHandler?: MountOptionHandler replaceRequest?: MountReplaceRequest } class Hono { get!: HandlerInterface post!: HandlerInterface put!: HandlerInterface delete!: HandlerInterface options!: HandlerInterface patch!: HandlerInterface all!: HandlerInterface on: OnHandlerInterface use: MiddlewareHandlerInterface /* This class is like an abstract class and does not have a router. To use it, inherit the class and implement router in the constructor. */ router!: Router<[H, RouterRoute]> readonly getPath: GetPath // Cannot use `#` because it requires visibility at JavaScript runtime. private _basePath: string = '/' #path: string = '/' routes: RouterRoute[] = [] constructor(options: HonoOptions = {}) { // Implementation of app.get(...handlers[]) or app.get(path, ...handlers[]) const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE] allMethods.forEach((method) => { this[method] = (args1: string | H, ...args: H[]) => { if (typeof args1 === 'string') { this.#path = args1 } else { this.addRoute(method, this.#path, args1) } args.forEach((handler) => { if (typeof handler !== 'string') { this.addRoute(method, this.#path, handler) } }) return this as any } }) // Implementation of app.on(method, path, ...handlers[]) this.on = (method: string | string[], path: string | string[], ...handlers: H[]) => { for (const p of [path].flat()) { this.#path = p for (const m of [method].flat()) { handlers.map((handler) => { this.addRoute(m.toUpperCase(), this.#path, handler) }) } } return this as any } // Implementation of app.use(...handlers[]) or app.use(path, ...handlers[]) this.use = (arg1: string | MiddlewareHandler, ...handlers: MiddlewareHandler[]) => { if (typeof arg1 === 'string') { this.#path = arg1 } else { this.#path = '*' handlers.unshift(arg1) } handlers.forEach((handler) => { this.addRoute(METHOD_NAME_ALL, this.#path, handler) }) return this as any } const strict = options.strict ?? true delete options.strict Object.assign(this, options) this.getPath = strict ? options.getPath ?? getPath : getPathNoStrict } private clone(): Hono { const clone = new Hono({ router: this.router, getPath: this.getPath, }) clone.routes = this.routes return clone } private notFoundHandler: NotFoundHandler = notFoundHandler private errorHandler: ErrorHandler = errorHandler /** * `.route()` allows grouping other Hono instance in routes. * * @see {@link https://hono.dev/docs/api/routing#grouping} * * @param {string} path - base Path * @param {Hono} app - other Hono instance * @returns {Hono} routed Hono instance * * @example * ```ts * const app = new Hono() * const app2 = new Hono() * * app2.get("/user", (c) => c.text("user")) * app.route("/api", app2) // GET /api/user * ``` */ route< SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string >( path: SubPath, app: Hono ): Hono> | S, BasePath> { const subApp = this.basePath(path) app.routes.map((r) => { let handler if (app.errorHandler === errorHandler) { handler = r.handler } else { handler = async (c: Context, next: Next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res ;(handler as any)[COMPOSED_HANDLER] = r.handler } subApp.addRoute(r.method, r.path, handler) }) return this } /** * `.basePath()` allows base paths to be specified. * * @see {@link https://hono.dev/docs/api/routing#base-path} * * @param {string} path - base Path * @returns {Hono} changed Hono instance * * @example * ```ts * const api = new Hono().basePath('/api') * ``` */ basePath(path: SubPath): Hono> { const subApp = this.clone() subApp._basePath = mergePath(this._basePath, path) return subApp } /** * `.onError()` handles an error and returns a customized Response. * * @see {@link https://hono.dev/docs/api/hono#error-handling} * * @param {ErrorHandler} handler - request Handler for error * @returns {Hono} changed Hono instance * * @example * ```ts * app.onError((err, c) => { * console.error(`${err}`) * return c.text('Custom Error Message', 500) * }) * ``` */ onError = (handler: ErrorHandler): Hono => { this.errorHandler = handler return this } /** * `.notFound()` allows you to customize a Not Found Response. * * @see {@link https://hono.dev/docs/api/hono#not-found} * * @param {NotFoundHandler} handler - request handler for not-found * @returns {Hono} changed Hono instance * * @example * ```ts * app.notFound((c) => { * return c.text('Custom 404 Message', 404) * }) * ``` */ notFound = (handler: NotFoundHandler): Hono => { this.notFoundHandler = handler return this } /** * `.mount()` allows you to mount applications built with other frameworks into your Hono application. * * @see {@link https://hono.dev/docs/api/hono#mount} * * @param {string} path - base Path * @param {Function} applicationHandler - other Request Handler * @param {MountOptions} [options] - options of `.mount()` * @returns {Hono} mounted Hono instance * * @example * ```ts * import { Router as IttyRouter } from 'itty-router' * import { Hono } from 'hono' * // Create itty-router application * const ittyRouter = IttyRouter() * // GET /itty-router/hello * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) * * const app = new Hono() * app.mount('/itty-router', ittyRouter.handle) * ``` * * @example * ```ts * const app = new Hono() * // Send the request to another application without modification. * app.mount('/app', anotherApp, { * replaceRequest: (req) => req, * }) * ``` */ mount( path: string, applicationHandler: (request: Request, ...args: any) => Response | Promise, options?: MountOptions ): Hono { // handle options let replaceRequest: MountReplaceRequest | undefined let optionHandler: MountOptionHandler | undefined if (options) { if (typeof options === 'function') { optionHandler = options } else { optionHandler = options.optionHandler replaceRequest = options.replaceRequest } } // prepare handlers for request const getOptions: (c: Context) => unknown[] = optionHandler ? (c) => { const options = optionHandler!(c) return Array.isArray(options) ? options : [options] } : (c) => { let executionContext: ExecutionContext | undefined = undefined try { executionContext = c.executionCtx } catch {} // Do nothing return [c.env, executionContext] } replaceRequest ||= (() => { const mergedPath = mergePath(this._basePath, path) const pathPrefixLength = mergedPath === '/' ? 0 : mergedPath.length return (request) => { const url = new URL(request.url) url.pathname = url.pathname.slice(pathPrefixLength) || '/' return new Request(url, request) } })() const handler: MiddlewareHandler = async (c, next) => { const res = await applicationHandler(replaceRequest!(c.req.raw), ...getOptions(c)) if (res) { return res } await next() } this.addRoute(METHOD_NAME_ALL, mergePath(path, '*'), handler) return this } private addRoute(method: string, path: string, handler: H) { method = method.toUpperCase() path = mergePath(this._basePath, path) const r: RouterRoute = { path: path, method: method, handler: handler } this.router.add(method, path, [handler, r]) this.routes.push(r) } private matchRoute(method: string, path: string) { return this.router.match(method, path) } private handleError(err: unknown, c: Context) { if (err instanceof Error) { return this.errorHandler(err, c) } throw err } private dispatch( request: Request, executionCtx: ExecutionContext | FetchEventLike | undefined, env: E['Bindings'], method: string ): Response | Promise { // Handle HEAD method if (method === 'HEAD') { return (async () => new Response(null, await this.dispatch(request, executionCtx, env, 'GET')))() } const path = this.getPath(request, { env }) const matchResult = this.matchRoute(method, path) const c = new Context(request, { path, matchResult, env, executionCtx, notFoundHandler: this.notFoundHandler, }) // Do not `compose` if it has only one handler if (matchResult[0].length === 1) { let res: ReturnType try { res = matchResult[0][0][0][0](c, async () => { c.res = await this.notFoundHandler(c) }) } catch (err) { return this.handleError(err, c) } return res instanceof Promise ? res .then( (resolved: Response | undefined) => resolved || (c.finalized ? c.res : this.notFoundHandler(c)) ) .catch((err: Error) => this.handleError(err, c)) : res ?? this.notFoundHandler(c) } const composed = compose(matchResult[0], this.errorHandler, this.notFoundHandler) return (async () => { try { const context = await composed(c) if (!context.finalized) { throw new Error( 'Context is not finalized. Did you forget to return a Response object or `await next()`?' ) } return context.res } catch (err) { return this.handleError(err, c) } })() } /** * `.fetch()` will be entry point of your app. * * @see {@link https://hono.dev/docs/api/hono#fetch} * * @param {Request} request - request Object of request * @param {Env} Env - env Object * @param {ExecutionContext} - context of execution * @returns {Response | Promise} response of request * */ fetch: ( request: Request, Env?: E['Bindings'] | {}, executionCtx?: ExecutionContext ) => Response | Promise = (request, ...rest) => { return this.dispatch(request, rest[1], rest[0], request.method) } /** * `.request()` is a useful method for testing. * You can pass a URL or pathname to send a GET request. * app will return a Response object. * ```ts * test('GET /hello is ok', async () => { * const res = await app.request('/hello') * expect(res.status).toBe(200) * }) * ``` * @see https://hono.dev/docs/api/hono#request */ request = ( input: RequestInfo | URL, requestInit?: RequestInit, Env?: E['Bindings'] | {}, executionCtx?: ExecutionContext ): Response | Promise => { if (input instanceof Request) { if (requestInit !== undefined) { input = new Request(input, requestInit) } return this.fetch(input, Env, executionCtx) } input = input.toString() const path = /^https?:\/\//.test(input) ? input : `http://localhost${mergePath('/', input)}` const req = new Request(path, requestInit) return this.fetch(req, Env, executionCtx) } /** * `.fire()` automatically adds a global fetch event listener. * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. * @see https://hono.dev/docs/api/hono#fire * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ */ fire = (): void => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore addEventListener('fetch', (event: FetchEventLike): void => { event.respondWith(this.dispatch(event.request, event, undefined, event.request.method)) }) } } export { Hono as HonoBase }