HonoRequest封装机制
要点
c.req是 Hono 对标准Request的包装层,c.req.raw指向原始 Request 对象- 路由参数并非从 URL 正则提取,而是来自路由器匹配阶段产出的
matchResult query()/queries()直接对 URL 字符串做优化解析,不会构造URLSearchParams- Body 读取方法通过
bodyCache实现缓存,允许以不同格式多次读取同一请求体 - Header 的大小写不敏感访问由标准
Headers对象保证,HonoRequest 本身不额外处理 parseBody()是 Hono 独有的便捷方法,将表单数据转成纯对象并支持嵌套键- 标准
Request.body是 ReadableStream,消费后不可回退;Hono 的缓存机制绕过了这个限制
1. HonoRequest 与标准 Request 的关系
上一篇讲了 Context 的整体结构,这篇聚焦 c.req。
当你拿到 c.req 的时候,你拿到的不是一个标准 Request,而是 Hono 自己定义的 HonoRequest 类的实例。它在标准 Request 之上封装了一层更便捷的 API。
// src/index.ts
app.get('/debug', (c) => {
// c.req — HonoRequest 实例
// c.req.raw — 标准的 Request 对象
console.log(c.req.raw instanceof Request) // true
console.log(c.req.raw.url) // 完整 URL
console.log(c.req.raw.method) // 'GET'
return c.text('OK')
})HonoRequest 的构造函数很简洁:
// 简化后的源码
class HonoRequest {
raw: Request
path: string
bodyCache = {}
constructor(request: Request, path = '/', matchResult = [[]]) {
this.raw = request
this.path = path
this.#matchResult = matchResult
}
}三个核心输入:原始 Request、当前路径 path、路由匹配结果 matchResult。后续所有便捷方法都围绕这三个数据展开。
c.req.raw 是你回到标准 API 的逃生舱口。当 HonoRequest 没提供你需要的能力时,直接操作 c.req.raw 就行——比如读取 Cloudflare Workers 特有的 cf 属性:
// src/index.ts
app.get('/geo', (c) => {
const cf = c.req.raw.cf // Cloudflare Workers 特有属性
return c.json({ country: cf?.country })
})2. 路径参数的解析
路由参数的来源可能和你想的不一样。
c.req.param('id') 并不是从 URL 里用正则匹配出来的。参数在路由匹配阶段就已经被提取好了,存在 matchResult 里。HonoRequest 只是从匹配结果中取值。
// src/index.ts
// GET /users/42
app.get('/users/:id', (c) => {
const id = c.req.param('id') // '42'
return c.json({ id })
})源码层面的逻辑大致是这样:
// 简化后的源码
param(key) {
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams()
}
#getDecodedParam(key) {
// 从 matchResult 中查找参数对应的值
const param = this.#matchResult[0][this.routeIndex][1][key]
const value = this.#getParamValue(param)
// 如果包含 % 编码,做一次 decodeURIComponent
return value && /\%/.test(value) ? tryDecodeURIComponent(value) : value
}注意两个细节:
- 不传参数调用
c.req.param()会返回所有参数的对象 - 值如果包含 URL 编码字符(
%开头的序列),会自动做decodeURIComponent
// src/index.ts
// GET /search/%E4%BB%8A%E6%97%A5
app.get('/search/:keyword', (c) => {
const all = c.req.param()
// { keyword: '今日' } — 已自动解码
return c.json(all)
})如果你手动对参数值再做一次 decode,反而会出错。Hono 已经替你处理过了。
3. 查询参数解析
c.req.query() 和 c.req.queries() 都定义在 HonoRequest 上,底层调用的是 utils/url.ts 中的 getQueryParam 和 getQueryParams。
// 源码
query(key) {
return getQueryParam(this.url, key)
}
queries(key) {
return getQueryParams(this.url, key)
}这两个函数直接对 URL 字符串做字符级解析,不会创建 URLSearchParams 对象。对于高频调用的场景,这种字符串扫描方式比构造对象再遍历要快。
query() 返回第一个匹配值:
// src/index.ts
// GET /search?q=hono&page=2
app.get('/search', (c) => {
const q = c.req.query('q') // 'hono'
const page = c.req.query('page') // '2'
// 不存在的 key 返回 undefined
const missing = c.req.query('sort') // undefined
return c.json({ q, page, missing })
})queries() 返回所有匹配值组成的数组,适用于同名参数出现多次的情况:
// src/index.ts
// GET /filter?tag=js&tag=ts&tag=rust
app.get('/filter', (c) => {
const tags = c.req.queries('tag')
// ['js', 'ts', 'rust']
// 单个值时也返回数组
// GET /filter?tag=js → ['js']
// 不存在时返回 undefined
const empty = c.req.queries('missing') // undefined
return c.json({ tags })
})这里有个容易混淆的点:queries() 在参数不存在时返回 undefined,不是空数组。如果你要做安全的遍历,需要先判断存在性。
4. Body 读取的各种方法
HonoRequest 提供了五种 body 读取方式:
// src/index.ts
app.post('/data', async (c) => {
// 1. JSON 解析
const json = await c.req.json()
// 2. 纯文本
const text = await c.req.text()
// 3. ArrayBuffer(二进制数据)
const buffer = await c.req.arrayBuffer()
// 4. Blob
const blob = await c.req.blob()
// 5. FormData(multipart/form-data 或 application/x-www-form-urlencoded)
const formData = await c.req.formData()
})前四个方法直接对应标准 Request 上的同名方法。第五个 parseBody() 是 Hono 独有的:
// src/index.ts
// POST Content-Type: application/x-www-form-urlencoded
// body: name=Alice&age=30
app.post('/form', async (c) => {
const data = await c.req.parseBody()
// { name: 'Alice', age: '30' }
return c.json(data)
})parseBody() 和 formData() 的区别在于:formData() 返回标准的 FormData 对象,你需要用 .get('key') 取值;parseBody() 返回纯 JavaScript 对象,可以直接用 data.key 访问。
parseBody() 还支持两个选项:
// src/index.ts
// POST body: tags[]=js&tags[]=ts&user.name=Bob
app.post('/complex', async (c) => {
const data = await c.req.parseBody({ all: true, dot: true })
// {
// tags: ['js', 'ts'], // all: true 将同名参数合并为数组
// user: { name: 'Bob' } // dot: true 将点号键展开为嵌套对象
// }
return c.json(data)
})parseBody() 内部会先检查 Content-Type,只有 multipart/form-data 和 application/x-www-form-urlencoded 才会解析,其他类型直接返回空对象。
5. bodyCache:Body 可重复读取的秘密
04 里提到「请求体通常只能读取一次」——这对标准 Request 是成立的,因为 Request.body 是一个 ReadableStream,消费后就没了。
但 HonoRequest 通过 bodyCache 改变了这个行为:
// 简化后的源码
#cachedBody = (key) => {
const { bodyCache, raw } = this
// 1. 如果缓存里已经有这个格式的结果,直接返回
if (bodyCache[key]) {
return bodyCache[key]
}
// 2. 如果缓存里有其他格式的结果,基于它做转换
const anyCachedKey = Object.keys(bodyCache)[0]
if (anyCachedKey) {
return bodyCache[anyCachedKey].then((body) => {
if (anyCachedKey === 'json') {
body = JSON.stringify(body)
}
return new Response(body)[key]()
})
}
// 3. 首次读取,从 raw Request 获取并缓存
return bodyCache[key] = raw[key]()
}这意味着:
// src/index.ts
app.post('/multi', async (c) => {
// 先读 JSON
const data = await c.req.json()
// 再读原始文本——能正常工作,因为 bodyCache 会复用之前的结果
const raw = await c.req.text()
// 两次读取都拿到正确内容
return c.json({ parsed: data, rawText: raw })
})但要注意一个限制:如果你绕过了 HonoRequest,直接用了 c.req.raw.text() 或 c.req.raw.json(),那就不会走 bodyCache,后续再调用 c.req.text() 也会失败——因为底层 stream 已经被你消费了。
// src/index.ts
app.post('/trap', async (c) => {
// 直接消费了 raw Request 的 body
const raw = await c.req.raw.text()
// 这里会出错——body 已经被上面消费
// const data = await c.req.json() // TypeError
return c.text(raw)
})规则很简单:要么全程用 c.req.json() / c.req.text() 等方法,要么全程用 c.req.raw 的方法,不要混用。
6. Header 访问
c.req.header() 的实现直接委托给标准 Headers 对象:
// 源码
header(name) {
if (name) {
return this.raw.headers.get(name) ?? undefined
}
// 不传参数,返回所有 header 的普通对象
const headerData = {}
this.raw.headers.forEach((value, key) => {
headerData[key] = value
})
return headerData
}大小写不敏感的特性不是 HonoRequest 实现的,是标准 Headers.get() 本身就支持:
// src/index.ts
app.get('/headers', (c) => {
// 以下三种写法等价,都能正确获取到值
const a = c.req.header('Content-Type')
const b = c.req.header('content-type')
const cc = c.req.header('CONTENT-TYPE')
// 不传参数,获取全部 headers
const all = c.req.header()
// { 'content-type': 'application/json', 'authorization': 'Bearer ...', ... }
return c.json({ a, b, cc })
})传了不存在的 header 名,返回 undefined(源码里用 ?? undefined 显式处理了 null 到 undefined 的转换)。
7. Cookie 解析
HonoRequest 本身不提供 c.req.cookie() 方法。Cookie 解析通过 hono/cookie 辅助模块提供:
// src/index.ts
import { getCookie, setCookie } from 'hono/cookie'
app.get('/dashboard', (c) => {
// 读取单个 cookie
const token = getCookie(c, 'session_token')
// 读取所有 cookie
const all = getCookie(c)
return c.json({ token })
})
app.post('/login', async (c) => {
// 设置 cookie
setCookie(c, 'session_token', 'abc123', {
httpOnly: true,
secure: true,
maxAge: 86400,
path: '/',
})
return c.text('Logged in')
})getCookie(c, 'name') 内部的实现是解析 c.req.raw.headers.get('Cookie') 字符串,按分号拆分后逐个匹配键值对。它还会做 Cookie 值的合法性校验——名字和值必须符合 RFC 规范才会被解析。
hono/cookie 还提供了签名 Cookie 的能力,用 HMAC-SHA256 防止客户端篡改:
// src/index.ts
import { getSignedCookie, setSignedCookie } from 'hono/cookie'
const SECRET = 'my-secret-key'
app.get('/verified', async (c) => {
// 验证签名后返回值,签名无效返回 undefined
const userId = await getSignedCookie(c, SECRET, 'user_id')
return c.json({ userId })
})8. url / path / method 等便捷属性
HonoRequest 上有几个 getter,直接代理到 raw 对象:
// 源码
get url() {
return this.raw.url
}
get method() {
return this.raw.method
}path 比较特殊,它不是 getter,而是构造函数里直接赋值的实例属性:
// 源码
class HonoRequest {
path: string // 构造函数中赋值
constructor(request, path = '/', matchResult = [[]]) {
this.path = path
}
}c.req.path 和 c.req.url 的区别:
// src/index.ts
// GET http://localhost:8787/users/42?page=1
app.get('/users/:id', (c) => {
c.req.url // 'http://localhost:8787/users/42?page=1' — 完整 URL
c.req.path // '/users/42' — 只有路径部分,不含 query 和 origin
c.req.method // 'GET'
return c.text('OK')
})c.req.path 的值在 Context 构造时就确定了,等于请求的实际路径(不是路由模式)。如果你需要获取路由注册时的模式路径(如 /users/:id),可以使用 matchedRoutes 辅助函数从 hono/route 获取。
延伸阅读
- 04-Context 上下文对象原理 — HonoRequest 在 Context 中的位置
- 02-路由匹配原理 — matchResult 是如何生成的
- Web Standards Request API — 标准 Request 的完整规范
- Hono Request 文档 — HonoRequest 的 API 参考
总结
HonoRequest 在标准 Request 之上做了三件事:
- 参数提取:路由参数从 matchResult 取值并自动 decode,查询参数用字符串扫描优化性能
- Body 缓存:通过 bodyCache 让同一次请求可以用不同格式多次读取,前提是全程使用 HonoRequest 的方法而不是直接操作 raw
- 便捷封装:path/method/url 等属性简化访问,parseBody() 提供表单到对象的直接转换,cookie 通过辅助模块支持
理解 bodyCache 的机制后,就能避免「body 只能读一次」的常见坑。规则记一条就够:从请求里读数据,始终用 c.req 的方法,不要碰 c.req.raw。