Very Popular
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368// Copyright 2018-2022 the oak authors. All rights reserved. MIT license.
import type { Application, State } from "./application.ts";import { createHttpError, KeyStack, SecureCookieMap, ServerSentEventStreamTarget, type ServerSentEventTarget, type ServerSentEventTargetOptions,} from "./deps.ts";import { Request } from "./request.ts";import { Response } from "./response.ts";import { send, SendOptions } from "./send.ts";import type { ErrorStatus, ServerRequest, UpgradeWebSocketOptions,} from "./types.d.ts";import { assert } from "./util.ts";
export interface ContextOptions< S extends AS = State, // deno-lint-ignore no-explicit-any AS extends State = Record<string, any>,> { jsonBodyReplacer?: ( key: string, value: unknown, context: Context<S>, ) => unknown; jsonBodyReviver?: ( key: string, value: unknown, context: Context<S>, ) => unknown; secure?: boolean;}
export interface ContextSendOptions extends SendOptions { /** The filename to send, which will be resolved based on the other options. * If this property is omitted, the current context's `.request.url.pathname` * will be used. */ path?: string;}
/** Provides context about the current request and response to middleware * functions, and the current instance being processed is the first argument * provided a {@linkcode Middleware} function. * * _Typically this is only used as a type annotation and shouldn't be * constructed directly._ * * ### Example * * ```ts * import { Application, Context } from "https://deno.land/x/oak/mod.ts"; * * const app = new Application(); * * app.use((ctx) => { * // information about the request is here: * ctx.request; * // information about the response is here: * ctx.response; * // the cookie store is here: * ctx.cookies; * }); * * // Needs a type annotation because it cannot be inferred. * function mw(ctx: Context) { * // process here... * } * * app.use(mw); * ``` * * @template S the state which extends the application state (`AS`) * @template AS the type of the state derived from the application */export class Context< S extends AS = State, // deno-lint-ignore no-explicit-any AS extends State = Record<string, any>,> { #socket?: WebSocket; #sse?: ServerSentEventTarget;
#wrapReviverReplacer( reviver?: (key: string, value: unknown, context: this) => unknown, ): undefined | ((key: string, value: unknown) => unknown) { return reviver ? (key: string, value: unknown) => reviver(key, value, this) : undefined; }
/** A reference to the current application. */ app: Application<AS>;
/** An object which allows access to cookies, mediating both the request and * response. */ cookies: SecureCookieMap;
/** Is `true` if the current connection is upgradeable to a web socket. * Otherwise the value is `false`. Use `.upgrade()` to upgrade the connection * and return the web socket. */ get isUpgradable(): boolean { const upgrade = this.request.headers.get("upgrade"); if (!upgrade || upgrade.toLowerCase() !== "websocket") { return false; } const secKey = this.request.headers.get("sec-websocket-key"); return typeof secKey === "string" && secKey != ""; }
/** Determines if the request should be responded to. If `false` when the * middleware completes processing, the response will not be sent back to the * requestor. Typically this is used if the middleware will take over low * level processing of requests and responses, for example if using web * sockets. This automatically gets set to `false` when the context is * upgraded to a web socket via the `.upgrade()` method. * * The default is `true`. */ respond: boolean;
/** An object which contains information about the current request. */ request: Request;
/** An object which contains information about the response that will be sent * when the middleware finishes processing. */ response: Response;
/** If the the current context has been upgraded, then this will be set to * with the current web socket, otherwise it is `undefined`. */ get socket(): WebSocket | undefined { return this.#socket; }
/** The object to pass state to front-end views. This can be typed by * supplying the generic state argument when creating a new app. For * example: * * ```ts * const app = new Application<{ foo: string }>(); * ``` * * Or can be contextually inferred based on setting an initial state object: * * ```ts * const app = new Application({ state: { foo: "bar" } }); * ``` * * On each request/response cycle, the context's state is cloned from the * application state. This means changes to the context's `.state` will be * dropped when the request drops, but "defaults" can be applied to the * application's state. Changes to the application's state though won't be * reflected until the next request in the context's state. */ state: S;
constructor( app: Application<AS>, serverRequest: ServerRequest, state: S, { secure = false, jsonBodyReplacer, jsonBodyReviver, }: ContextOptions<S, AS> = {}, ) { this.app = app; this.state = state; const { proxy } = app; this.request = new Request( serverRequest, { proxy, secure, jsonBodyReviver: this.#wrapReviverReplacer(jsonBodyReviver), }, ); this.respond = true; this.response = new Response( this.request, this.#wrapReviverReplacer(jsonBodyReplacer), ); this.cookies = new SecureCookieMap(serverRequest, { keys: this.app.keys as KeyStack | undefined, response: this.response, secure: this.request.secure, }); }
/** Asserts the condition and if the condition fails, creates an HTTP error * with the provided status (which defaults to `500`). The error status by * default will be set on the `.response.status`. * * Because of limitation of TypeScript, any assertion type function requires * specific type annotations, so the {@linkcode Context} type should be used * even if it can be inferred from the context. * * ### Example * * ```ts * import { Context, Status } from "https://deno.land/x/oak/mod.ts"; * * export function mw(ctx: Context) { * const body = ctx.request.body(); * ctx.assert(body.type === "json", Status.NotAcceptable); * // process the body and send a response... * } * ``` */ assert( // deno-lint-ignore no-explicit-any condition: any, errorStatus: ErrorStatus = 500, message?: string, props?: Record<string, unknown>, ): asserts condition { if (condition) { return; } const err = createHttpError(errorStatus, message); if (props) { Object.assign(err, props); } throw err; }
/** Asynchronously fulfill a response with a file from the local file * system. * * If the `options.path` is not supplied, the file to be sent will default * to this `.request.url.pathname`. * * Requires Deno read permission. */ send(options: ContextSendOptions): Promise<string | undefined> { const { path = this.request.url.pathname, ...sendOptions } = options; return send(this, path, sendOptions); }
/** Convert the connection to stream events, returning an event target for * sending server sent events. Events dispatched on the returned target will * be sent to the client and be available in the client's `EventSource` that * initiated the connection. * * **Note** the body needs to be returned to the client to be able to * dispatch events, so dispatching events within the middleware will delay * sending the body back to the client. * * This will set the response body and update response headers to support * sending SSE events. Additional middleware should not modify the body. */ sendEvents(options?: ServerSentEventTargetOptions): ServerSentEventTarget { if (!this.#sse) { assert(this.response.writable, "The response is not writable."); const sse = this.#sse = new ServerSentEventStreamTarget(options); this.app.addEventListener("close", () => sse.close()); const [bodyInit, { headers }] = sse.asResponseInit({ headers: this.response.headers, }); this.response.body = bodyInit; if (headers instanceof Headers) { this.response.headers = headers; } } return this.#sse; }
/** Create and throw an HTTP Error, which can be used to pass status * information which can be caught by other middleware to send more * meaningful error messages back to the client. The passed error status will * be set on the `.response.status` by default as well. */ throw( errorStatus: ErrorStatus, message?: string, props?: Record<string, unknown>, ): never { const err = createHttpError(errorStatus, message); if (props) { Object.assign(err, props); } throw err; }
/** Take the current request and upgrade it to a web socket, resolving with * the a web standard `WebSocket` object. This will set `.respond` to * `false`. If the socket cannot be upgraded, this method will throw. */ upgrade(options?: UpgradeWebSocketOptions): WebSocket { if (this.#socket) { return this.#socket; } if (!this.request.originalRequest.upgrade) { throw new TypeError( "Web socket upgrades not currently supported for this type of server.", ); } this.#socket = this.request.originalRequest.upgrade(options); this.app.addEventListener("close", () => this.#socket?.close()); this.respond = false; return this.#socket; }
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) { const { app, cookies, isUpgradable, respond, request, response, socket, state, } = this; return `${this.constructor.name} ${ inspect({ app, cookies, isUpgradable, respond, request, response, socket, state, }) }`; }
[Symbol.for("nodejs.util.inspect.custom")]( depth: number, // deno-lint-ignore no-explicit-any options: any, inspect: (value: unknown, options?: unknown) => string, ) { if (depth < 0) { return options.stylize(`[${this.constructor.name}]`, "special"); }
const newOptions = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1, }); const { app, cookies, isUpgradable, respond, request, response, socket, state, } = this; return `${options.stylize(this.constructor.name, "special")} ${ inspect({ app, cookies, isUpgradable, respond, request, response, socket, state, }, newOptions) }`; }}