context.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. // src/context.ts
  2. import { HonoRequest } from "./request.js";
  3. import { HtmlEscapedCallbackPhase, resolveCallback } from "./utils/html.js";
  4. var TEXT_PLAIN = "text/plain; charset=UTF-8";
  5. var setDefaultContentType = (contentType, headers) => {
  6. return {
  7. "Content-Type": contentType,
  8. ...headers
  9. };
  10. };
  11. var createResponseInstance = (body, init) => new Response(body, init);
  12. var Context = class {
  13. #rawRequest;
  14. #req;
  15. /**
  16. * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
  17. *
  18. * @see {@link https://hono.dev/docs/api/context#env}
  19. *
  20. * @example
  21. * ```ts
  22. * // Environment object for Cloudflare Workers
  23. * app.get('*', async c => {
  24. * const counter = c.env.COUNTER
  25. * })
  26. * ```
  27. */
  28. env = {};
  29. #var;
  30. finalized = false;
  31. /**
  32. * `.error` can get the error object from the middleware if the Handler throws an error.
  33. *
  34. * @see {@link https://hono.dev/docs/api/context#error}
  35. *
  36. * @example
  37. * ```ts
  38. * app.use('*', async (c, next) => {
  39. * await next()
  40. * if (c.error) {
  41. * // do something...
  42. * }
  43. * })
  44. * ```
  45. */
  46. error;
  47. #status;
  48. #executionCtx;
  49. #res;
  50. #layout;
  51. #renderer;
  52. #notFoundHandler;
  53. #preparedHeaders;
  54. #matchResult;
  55. #path;
  56. /**
  57. * Creates an instance of the Context class.
  58. *
  59. * @param req - The Request object.
  60. * @param options - Optional configuration options for the context.
  61. */
  62. constructor(req, options) {
  63. this.#rawRequest = req;
  64. if (options) {
  65. this.#executionCtx = options.executionCtx;
  66. this.env = options.env;
  67. this.#notFoundHandler = options.notFoundHandler;
  68. this.#path = options.path;
  69. this.#matchResult = options.matchResult;
  70. }
  71. }
  72. /**
  73. * `.req` is the instance of {@link HonoRequest}.
  74. */
  75. get req() {
  76. this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
  77. return this.#req;
  78. }
  79. /**
  80. * @see {@link https://hono.dev/docs/api/context#event}
  81. * The FetchEvent associated with the current request.
  82. *
  83. * @throws Will throw an error if the context does not have a FetchEvent.
  84. */
  85. get event() {
  86. if (this.#executionCtx && "respondWith" in this.#executionCtx) {
  87. return this.#executionCtx;
  88. } else {
  89. throw Error("This context has no FetchEvent");
  90. }
  91. }
  92. /**
  93. * @see {@link https://hono.dev/docs/api/context#executionctx}
  94. * The ExecutionContext associated with the current request.
  95. *
  96. * @throws Will throw an error if the context does not have an ExecutionContext.
  97. */
  98. get executionCtx() {
  99. if (this.#executionCtx) {
  100. return this.#executionCtx;
  101. } else {
  102. throw Error("This context has no ExecutionContext");
  103. }
  104. }
  105. /**
  106. * @see {@link https://hono.dev/docs/api/context#res}
  107. * The Response object for the current request.
  108. */
  109. get res() {
  110. return this.#res ||= createResponseInstance(null, {
  111. headers: this.#preparedHeaders ??= new Headers()
  112. });
  113. }
  114. /**
  115. * Sets the Response object for the current request.
  116. *
  117. * @param _res - The Response object to set.
  118. */
  119. set res(_res) {
  120. if (this.#res && _res) {
  121. _res = createResponseInstance(_res.body, _res);
  122. for (const [k, v] of this.#res.headers.entries()) {
  123. if (k === "content-type") {
  124. continue;
  125. }
  126. if (k === "set-cookie") {
  127. const cookies = this.#res.headers.getSetCookie();
  128. _res.headers.delete("set-cookie");
  129. for (const cookie of cookies) {
  130. _res.headers.append("set-cookie", cookie);
  131. }
  132. } else {
  133. _res.headers.set(k, v);
  134. }
  135. }
  136. }
  137. this.#res = _res;
  138. this.finalized = true;
  139. }
  140. /**
  141. * `.render()` can create a response within a layout.
  142. *
  143. * @see {@link https://hono.dev/docs/api/context#render-setrenderer}
  144. *
  145. * @example
  146. * ```ts
  147. * app.get('/', (c) => {
  148. * return c.render('Hello!')
  149. * })
  150. * ```
  151. */
  152. render = (...args) => {
  153. this.#renderer ??= (content) => this.html(content);
  154. return this.#renderer(...args);
  155. };
  156. /**
  157. * Sets the layout for the response.
  158. *
  159. * @param layout - The layout to set.
  160. * @returns The layout function.
  161. */
  162. setLayout = (layout) => this.#layout = layout;
  163. /**
  164. * Gets the current layout for the response.
  165. *
  166. * @returns The current layout function.
  167. */
  168. getLayout = () => this.#layout;
  169. /**
  170. * `.setRenderer()` can set the layout in the custom middleware.
  171. *
  172. * @see {@link https://hono.dev/docs/api/context#render-setrenderer}
  173. *
  174. * @example
  175. * ```tsx
  176. * app.use('*', async (c, next) => {
  177. * c.setRenderer((content) => {
  178. * return c.html(
  179. * <html>
  180. * <body>
  181. * <p>{content}</p>
  182. * </body>
  183. * </html>
  184. * )
  185. * })
  186. * await next()
  187. * })
  188. * ```
  189. */
  190. setRenderer = (renderer) => {
  191. this.#renderer = renderer;
  192. };
  193. /**
  194. * `.header()` can set headers.
  195. *
  196. * @see {@link https://hono.dev/docs/api/context#header}
  197. *
  198. * @example
  199. * ```ts
  200. * app.get('/welcome', (c) => {
  201. * // Set headers
  202. * c.header('X-Message', 'Hello!')
  203. * c.header('Content-Type', 'text/plain')
  204. *
  205. * return c.body('Thank you for coming')
  206. * })
  207. * ```
  208. */
  209. header = (name, value, options) => {
  210. if (this.finalized) {
  211. this.#res = createResponseInstance(this.#res.body, this.#res);
  212. }
  213. const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers();
  214. if (value === void 0) {
  215. headers.delete(name);
  216. } else if (options?.append) {
  217. headers.append(name, value);
  218. } else {
  219. headers.set(name, value);
  220. }
  221. };
  222. status = (status) => {
  223. this.#status = status;
  224. };
  225. /**
  226. * `.set()` can set the value specified by the key.
  227. *
  228. * @see {@link https://hono.dev/docs/api/context#set-get}
  229. *
  230. * @example
  231. * ```ts
  232. * app.use('*', async (c, next) => {
  233. * c.set('message', 'Hono is hot!!')
  234. * await next()
  235. * })
  236. * ```
  237. */
  238. set = (key, value) => {
  239. this.#var ??= /* @__PURE__ */ new Map();
  240. this.#var.set(key, value);
  241. };
  242. /**
  243. * `.get()` can use the value specified by the key.
  244. *
  245. * @see {@link https://hono.dev/docs/api/context#set-get}
  246. *
  247. * @example
  248. * ```ts
  249. * app.get('/', (c) => {
  250. * const message = c.get('message')
  251. * return c.text(`The message is "${message}"`)
  252. * })
  253. * ```
  254. */
  255. get = (key) => {
  256. return this.#var ? this.#var.get(key) : void 0;
  257. };
  258. /**
  259. * `.var` can access the value of a variable.
  260. *
  261. * @see {@link https://hono.dev/docs/api/context#var}
  262. *
  263. * @example
  264. * ```ts
  265. * const result = c.var.client.oneMethod()
  266. * ```
  267. */
  268. // c.var.propName is a read-only
  269. get var() {
  270. if (!this.#var) {
  271. return {};
  272. }
  273. return Object.fromEntries(this.#var);
  274. }
  275. #newResponse(data, arg, headers) {
  276. const responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders ?? new Headers();
  277. if (typeof arg === "object" && "headers" in arg) {
  278. const argHeaders = arg.headers instanceof Headers ? arg.headers : new Headers(arg.headers);
  279. for (const [key, value] of argHeaders) {
  280. if (key.toLowerCase() === "set-cookie") {
  281. responseHeaders.append(key, value);
  282. } else {
  283. responseHeaders.set(key, value);
  284. }
  285. }
  286. }
  287. if (headers) {
  288. for (const [k, v] of Object.entries(headers)) {
  289. if (typeof v === "string") {
  290. responseHeaders.set(k, v);
  291. } else {
  292. responseHeaders.delete(k);
  293. for (const v2 of v) {
  294. responseHeaders.append(k, v2);
  295. }
  296. }
  297. }
  298. }
  299. const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
  300. return createResponseInstance(data, { status, headers: responseHeaders });
  301. }
  302. newResponse = (...args) => this.#newResponse(...args);
  303. /**
  304. * `.body()` can return the HTTP response.
  305. * You can set headers with `.header()` and set HTTP status code with `.status`.
  306. * This can also be set in `.text()`, `.json()` and so on.
  307. *
  308. * @see {@link https://hono.dev/docs/api/context#body}
  309. *
  310. * @example
  311. * ```ts
  312. * app.get('/welcome', (c) => {
  313. * // Set headers
  314. * c.header('X-Message', 'Hello!')
  315. * c.header('Content-Type', 'text/plain')
  316. * // Set HTTP status code
  317. * c.status(201)
  318. *
  319. * // Return the response body
  320. * return c.body('Thank you for coming')
  321. * })
  322. * ```
  323. */
  324. body = (data, arg, headers) => this.#newResponse(data, arg, headers);
  325. /**
  326. * `.text()` can render text as `Content-Type:text/plain`.
  327. *
  328. * @see {@link https://hono.dev/docs/api/context#text}
  329. *
  330. * @example
  331. * ```ts
  332. * app.get('/say', (c) => {
  333. * return c.text('Hello!')
  334. * })
  335. * ```
  336. */
  337. text = (text, arg, headers) => {
  338. return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(
  339. text,
  340. arg,
  341. setDefaultContentType(TEXT_PLAIN, headers)
  342. );
  343. };
  344. /**
  345. * `.json()` can render JSON as `Content-Type:application/json`.
  346. *
  347. * @see {@link https://hono.dev/docs/api/context#json}
  348. *
  349. * @example
  350. * ```ts
  351. * app.get('/api', (c) => {
  352. * return c.json({ message: 'Hello!' })
  353. * })
  354. * ```
  355. */
  356. json = (object, arg, headers) => {
  357. return this.#newResponse(
  358. JSON.stringify(object),
  359. arg,
  360. setDefaultContentType("application/json", headers)
  361. );
  362. };
  363. html = (html, arg, headers) => {
  364. const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
  365. return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
  366. };
  367. /**
  368. * `.redirect()` can Redirect, default status code is 302.
  369. *
  370. * @see {@link https://hono.dev/docs/api/context#redirect}
  371. *
  372. * @example
  373. * ```ts
  374. * app.get('/redirect', (c) => {
  375. * return c.redirect('/')
  376. * })
  377. * app.get('/redirect-permanently', (c) => {
  378. * return c.redirect('/', 301)
  379. * })
  380. * ```
  381. */
  382. redirect = (location, status) => {
  383. const locationString = String(location);
  384. this.header(
  385. "Location",
  386. // Multibyes should be encoded
  387. // eslint-disable-next-line no-control-regex
  388. !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)
  389. );
  390. return this.newResponse(null, status ?? 302);
  391. };
  392. /**
  393. * `.notFound()` can return the Not Found Response.
  394. *
  395. * @see {@link https://hono.dev/docs/api/context#notfound}
  396. *
  397. * @example
  398. * ```ts
  399. * app.get('/notfound', (c) => {
  400. * return c.notFound()
  401. * })
  402. * ```
  403. */
  404. notFound = () => {
  405. this.#notFoundHandler ??= () => createResponseInstance();
  406. return this.#notFoundHandler(this);
  407. };
  408. };
  409. export {
  410. Context,
  411. TEXT_PLAIN
  412. };