首页
/ Koa Request 对象 API 全解析:从请求头到内容协商的源码级实践指南

Koa Request 对象 API 全解析:从请求头到内容协商的源码级实践指南

2026-09-08 11:19:06作者:劳婵绚Shirley

Koa 的 Request 对象是对 Node.js 原生请求对象(http.IncomingMessage)的轻量抽象,它通过 ES2017 async 函数风格的 API,为日常 HTTP 服务开发补齐了请求头解析、URL 与查询串读写、缓存协商、代理信任、内容协商等高频能力。阅读本文后,你将掌握 ctx.request 上全部 getter/setter 与方法的语义、底层实现与适用边界,并能在真实中间件中正确使用 ctx.get()ctx.accepts()ctx.is()ctx.ips 等工具处理请求。

本文对应的 API 权威出处为 docs/api/request.md,实现源码为 lib/request.js,测试用例位于 tests/request/tests/application/ 目录。仓库当前版本为 koa 3.2.1(见 package.json)。

理解 Request 对象与 ctx 的关系

Koa 文档把请求相关能力集中描述为 Request 对象,但日常代码里多数时候直接写在 ctx 上。二者的一致性来自委托机制:在 lib/context.jsdelegate(proto, 'request') 声明中,method('accepts')method('is')method('get')access('querystring')access('query')access('path')access('url')access('method'),以及 getter('origin')getter('href')getter('protocol')getter('host')getter('hostname')getter('header')getter('secure')getter('fresh')getter('ips')getter('ip') 等都被逐项转发。

也就是说:

ctx.request.header === ctx.header       // true
ctx.request.query   === ctx.query       // true
ctx.request.get('X') === ctx.get('X')   // true

应用层在 lib/application.jscreateContext() 中,把 this.request 原型通过 Object.create 实例化并挂到 context.request 上,同时回填 request.ctxrequest.apprequest.reqrequest.response,因此 Request 的 getter 能反向访问到 ctxapp 与原生 req

请求头:header / headers / get(field)

读取与整体替换

request.headerrequest.headers 是完全等价的一对别名,返回与 Node 原生请求一致的请求头对象;文档还提供了对应的 setter(request.header= / request.headers=)用于整体替换。源码中它们都直接读写 this.req.headerslib/request.js),因此类型为普通对象,键名小写、值为字符串或字符串数组。

ctx.header          // => { 'user-agent': 'curl/8.0', host: 'localhost:3000', ... }
ctx.request.header  // 等价

单个头:request.get(field)

按字段名取单个请求头,大小写不敏感。实现上对 referer / referrer 做了特殊处理——两个拼写互通(lib/request.js):

ctx.get('Content-Type')  // => 'text/plain'
ctx.get('content-type')  // 同样命中
ctx.get('Referer')       // 兼容拼写 'referrer' 与 'referer'
ctx.get('Something')     // 无此头时返回 ''

注意取不到时返回的是空字符串而非 undefined,这与 HTTP 模块返回 undefined 不同,做判空时不要写 if (ctx.get('X-Foo')) 之外依赖 undefined 的代码。

方法与 URL 族:method、url、originalUrl、href、path、origin

method 读写

request.method 对应 req.method。setter 存在意义在于实现 methodOverride() 这类中间件:先在请求体/自定义头里读取“伪装方法”,再把 ctx.method 改写为真实的 PUT/DELETE 后继续放行到下游中间件。

ctx.method       // => 'POST'
ctx.method = 'DELETE'

URL 相关

  • request.urlreq.url 的直通读写,通常为不含协议与 host 的路径加查询串(如 /foo/bar?q=1)。setter 常用于 URL 重写。
  • request.originalUrl:请求到达时的原始 URL,在 createContext() 中由 req.url 固化(lib/application.js)。即便此后通过 ctx.url=ctx.path=ctx.query= 改写,originalUrl 也保持不变,见测试 tests/request/query.test.js 中 “should change .url but not .originalUrl” 用例。
  • request.origin:只取 origin 请求头(如 http://example.com),无此头返回 null;它并不拼接 host,注意与下面 href 区分。
  • request.href:完整 URL。若 originalUrl 本身是 http(s):// 开头的绝对地址则直接返回,否则拼装 protocol + '://' + host + originalUrllib/request.js)。
ctx.request.origin
// => http://example.com
ctx.request.href
// => http://example.com/foo/bar?q=1
  • request.path:路径名(不含查询串),底层经 parseurl 解析 req 得到 pathname。setter 会保留已有的查询串——实现是先解析 URL、改 pathname 并清空 path 缓存后用 url.format 重新序列化写回 this.urllib/request.js)。
// 假设当前 URL 是 /shop?page=2
ctx.path = '/store'   // ctx.url 变为 /store?page=2
ctx.path              // => '/store'

查询字符串三件套:query、querystring、search

三个属性分别呈现查询串的不同形态,全部提供读写:

属性 含义 示例(URL=/foo?page=2&color=blue
request.querystring 原始查询串,不含 ? 'page=2&color=blue'
request.search 原始查询串,含前导 ? '?page=2&color=blue'
request.query 解析后的对象 { page: '2', color: 'blue' }
  • search 的实现就是 querystring 加前导 ?(为空时返回 '');search=querystring= 等价(lib/request.js)。
  • query getter 调用本仓库自研的 lib/search-params.jsparse(),它基于 WHATWG URLSearchParams:单值键返回字符串,多值键返回数组;结果按原始查询串做内存缓存this._querycache),同一 URL 多次读取返回同一对象(测试 tests/request/query.test.js 验证了“每次访问返回同一对象”)。
  • 文档明确:query 的 getter 与 setter 都不支持嵌套对象解析query= 会把对象用 URLSearchParams 序列化(数字会被当作字符串、数组键会展开成多个同名参数)后写回 URL,测试见 ctx.query = { page: 2, color: 'blue' } 得到 /store/shoes?page=2&color=blue
ctx.query = { next: '/login' };   // ctx.url 变为 ...?next=%2Flogin

如需嵌套查询,可自行引入 qs 等库,这已超出 Koa 默认行为。

长度、类型与字符集:length、type、charset

  • request.length:把 Content-Length 头解析为数字返回;头缺失返回 undefined,非法值也返回 undefinedparseInt 后做 NaN 判断,见 lib/request.js)。
  • request.type:取 Content-Type 并去掉参数部分(以 ; 截断),无该头返回 ''
const ct = ctx.request.type;
// => "image/png"     (即使原始值为 image/png; charset=utf-8)
  • request.charset:用 content-type 模块解析 Content-Type 参数,返回 charset 参数值,无则返回 '';解析失败(如非法头)也返回 ''lib/request.js):
ctx.request.charset;
// => 'utf-8'

缓存协商:fresh / stale

request.fresh 用于判断请求缓存是否“新鲜”(资源内容未变化),服务于 If-None-Match/ETagIf-Modified-Since/Last-Modified 两对缓存协商头。它必须在你设置好对应响应头之后再读取。

判定条件(实现见 lib/request.js):

  1. 仅对 GET / HEAD 生效,其余方法直接返回 false
  2. 响应状态码必须是 2xx304
  3. 委托 fresh 库比较请求头与响应头是否匹配。

stale 就是 !fresh。典型用法来自文档:

// freshness check requires status 20x or 304
ctx.status = 200;
ctx.set('ETag', '123');

// cache is ok
if (ctx.fresh) {
  ctx.status = 304;
  return;
}

// cache is stale
// fetch new data
ctx.body = await db.find('something');

tests/request/fresh.test.js 覆盖了三种关键分支:POST 请求恒为 false、404 响应恒为 false、200 + ETag 匹配返回 true

代理与客户端标识:protocol、secure、ip、ips

这一族属性都受应用级开关 app.proxy 控制——只有当 app.proxytrue 时 Koa 才信任各 X-Forwarded-* 头。Koa 默认不信任任何代理头,避免未设置代理时被伪造(lib/application.js 中默认 proxy = false)。

  • request.protocol:优先看底层 socket 是否 TLS 加密(返回 'https');否则在 app.proxy 开启时信任 X-Forwarded-Proto(取逗号分隔第一个值并去空白,见辅助函数 splitCommaSeparatedValues);都没有则 'http'
  • request.secureprotocol === 'https' 的简写,常用于给 Cookie 打 Secure 标记等场景。
  • request.ips:开启 proxy 后读取 IP 列表头(默认 X-Forwarded-For),按逗号切分为数组,顺序为上游 → 下游。例:头值为 "client, proxy1, proxy2" 时返回 ["client", "proxy1", "proxy2"]。关闭 proxy 时返回空数组。
  • request.ipips 的第一项(最接近客户端的地址)优先,否则回退到 socket 的 remoteAddress。实现带 Symbol('context#ip') 缓存并支持 setter 覆写(lib/request.js)。

X-Forwarded-For 伪造风险与两个官方缓解项

文档专门提醒:多数反向代理(如 nginx)用 proxy_add_x_forwarded_for 追加 X-Forwarded-For,这存在安全风险——恶意客户端可以预先伪造 X-Forwarded-For 头,经代理转发后 request.ips 会变成 ['forged', 'client', 'proxy1', 'proxy2'],从而绕过按 IP 做的限流/封禁。Koa 提供两个选项:

方案一:更换读取的 IP 头(app.proxyIpHeader。若能控制反向代理,可让代理把真实客户端 IP 写入自定义头(如 X-Real-IP),并配置 Koa 不再读 x-forwarded-for

const app = new Koa({
  proxy: true,
  proxyIpHeader: 'X-Real-IP',
});

方案二:限制信任的 IP 数量(app.maxIpsCount。若明确知道服务器前面有几层代理,只信任最下游的那几个 IP,从而忽略用户伪造的部分:

const app = new Koa({
  proxy: true,
  maxIpsCount: 1, // only one proxy in front of the server
});

// request.header['X-Forwarded-For'] === [ '127.0.0.1', '127.0.0.2' ];
// ctx.ips === [ '127.0.0.2' ];

源码层面对应 lib/request.jsipsthis.app.proxyIpHeader 指定的头取逗号分隔值,且当 this.app.maxIpsCount > 0 时执行 ips.slice(-this.app.maxIpsCount) 只保留末尾 N 个;tests/request/ips.test.js 完整验证了未信任代理被忽略、proxyIpHeader 生效、maxIpsCount 只取下游 IP 三组行为。

host 与 hostname

  • request.host:返回 hostname:port(含端口)。取值优先级(lib/request.js):

    1. app.proxy 开启时优先取 X-Forwarded-Host(取逗号分隔的首个值);
    2. HTTP/2(req.httpVersionMajor >= 2)下回退用 :authority 伪头;
    3. 最后回退 Host 头;
    4. 都不存在返回 ''

    另外,由于 Host 头按 RFC 7230 不允许携带 userinfo,为防御 evil.com:fake@legitimate.com 这类注入,当 host 中含 @ 时 Koa 会用 new URL('http://' + host).host 重新解析出真正的 host 部分,解析失败返回 ''——详见 tests/request/host.test.js 中 userinfo 相关用例。

  • request.hostname:在 host 基础上剥掉端口:IPv6([ 开头)走 WHATWG URL 解析得到 hostname,其余用 split(':', 1)[0] 截取。

  • request.URL:将 protocol + '://' + host + originalUrl 交给 WHATWG URL 构造后得到的标准 URL 对象,带惰性缓存(memoizedURL);构造失败时返回空对象(lib/request.js)。当需要 .pathname.searchParams 等标准字段时可复用该对象,避免重复解析。

子域:subdomains 与 app.subdomainOffset

request.subdomains 返回子域数组。所谓子域即主应用域之前、以点分隔的 host 片段。默认认为主域是 host 的最后两段(app.subdomainOffset 默认 2),该阈值可通过 app.subdomainOffset 调整。

例如 host 为 tobi.ferrets.example.com

  • 未设置 subdomainOffset(默认 2):ctx.subdomains["ferrets", "tobi"]
  • app.subdomainOffset 为 3:只剩 ["tobi"]

实现先把 hostname 按 . 切分、reverse()slice(offset)lib/request.js);当 hostname 是 IP 地址(经 net.isIP 判断,含带端口的 IP)时返回空数组。行为均由 tests/request/subdomains.test.js 佐证:

const app = new Koa({ subdomainOffset: 3 });
// 对 tobi.ferrets.example.com 的请求
ctx.subdomains  // => ['tobi']

幂等判断:idempotent

request.idempotent 判断请求是否幂等——内置集合为 ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']lib/request.js)。幂等方法可以安全重试,常用于网络重试策略:

if (ctx.idempotent) {
  // 可安全进行自动重试的业务分支
}

底层连接与 socket

request.socket 返回底层请求 socket(即 req.socket),常用于读取本地地址、判断连接是否 TLS、或挂接连接级事件。通过 context 委托,ctx.socket 同样可用。

内容协商:request.is(types...)

request.is(...) 检查请求的 Content-Type 是否命中给定的 MIME 类型,底层委托 type-is 库(lib/request.js)。返回值语义:

  • 无请求体 → 返回 null
  • 无 Content-Type 头或全部不匹配 → 返回 false
  • 匹配 → 返回命中的内容类型字符串。

支持 mime 全名、扩展名与 * 通配(由 type-is 提供):

// With Content-Type: text/html; charset=utf-8
ctx.is('html'); // => 'html'
ctx.is('text/html'); // => 'text/html'
ctx.is('text/*', 'text/html'); // => 'text/html'

// When Content-Type is application/json
ctx.is('json', 'urlencoded'); // => 'json'
ctx.is('application/json'); // => 'application/json'
ctx.is('html', 'application/*'); // => 'application/json'

ctx.is('html'); // => false

真实场景示例——只允许图片到达某路由,否则直接 415:

if (ctx.is('image/*')) {
  // process
} else {
  ctx.throw(415, 'images only!');
}

内容协商:accepts 家族(Accept 系列头)

Koa 基于 acceptsnegotiator 封装了四个内容协商工具:request.acceptsrequest.acceptsEncodingsrequest.acceptsCharsetsrequest.acceptsLanguages。它们背后的 accept 对象惰性创建并缓存于 this._acceptlib/request.js)。统一约定

  • 不传任何类型时,返回客户端所有可接受类型(按优先级排序的数组);
  • 传入多个类型时返回最佳匹配;若无匹配返回 false,此时应向客户端回 406 "Not Acceptable"
  • 客户端缺少相应 Accept 头(任何类型都接受)时返回你提供的第一个类型——因此传入顺序很重要。

request.accepts(types)

type 可以是完整 mime("application/json")、扩展名("json")或数组(["json", "html", "text/plain"]):

// Accept: text/html
ctx.accepts('html');
// => "html"

// Accept: text/*, application/json
ctx.accepts('html');
// => "html"
ctx.accepts('text/html');
// => "text/html"
ctx.accepts('json', 'text');
// => "json"
ctx.accepts('application/json');
// => "application/json"

// Accept: text/*, application/json
ctx.accepts('image/png');
ctx.accepts('png');
// => false

// Accept: text/*;q=.5, application/json
ctx.accepts(['html', 'json']);
ctx.accepts('html', 'json');
// => "json"

// No Accept header
ctx.accepts('html', 'json');
// => "html"
ctx.accepts('json', 'html');
// => "json"

最后一个示例说明“无 Accept 头时返回第一个类型”,所以顺序必须体现服务端偏好。accepts 可反复调用,也适合用 switch

switch (ctx.accepts('json', 'html', 'text')) {
  case 'json': break;
  case 'html': break;
  case 'text': break;
  default: ctx.throw(406, 'json, html, or text only');
}

request.acceptsEncodings(encodings)

针对 Accept-Encoding。文档特别提醒:务必把 identity(无编码)也放进候选,否则处理不了未压缩请求:

// Accept-Encoding: gzip
ctx.acceptsEncodings('gzip', 'deflate', 'identity');
// => "gzip"

ctx.acceptsEncodings(['gzip', 'deflate', 'identity']);
// => "gzip"

// Accept-Encoding: gzip, deflate
ctx.acceptsEncodings();
// => ["gzip", "deflate", "identity"]

边界情况:客户端若显式发送 identity;q=0identity 就不可接受,此时方法可能返回 false(虽然少见,仍需处理)。

request.acceptsCharsets(charsets)

针对 Accept-Charset

// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
ctx.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"

ctx.acceptsCharsets(['utf-7', 'utf-8']);
// => "utf-8"

ctx.acceptsCharsets();
// => ["utf-8", "utf-7", "iso-8859-1"]

注意不传参时的返回顺序是按质量值排序后的数组,与传入参数无关。

request.acceptsLanguages(langs)

针对 Accept-Language

// Accept-Language: en;q=0.8, es, pt
ctx.acceptsLanguages('es', 'en');
// => "es"

ctx.acceptsLanguages(['en', 'es']);
// => "es"

ctx.acceptsLanguages();
// => ["es", "pt", "en"]

序列化与调试:inspect / toJSON

request.toJSON() 通过 lib/only.js 仅提取 methodurlheader 三个字段(lib/request.js),inspect() 则等价返回该结果,并适配了新版 Node 的 util.inspect.custom 钩子,方便控制台打印请求对象时不泄露原生 req 内部结构。context 层的 lib/context.jstoJSON() 中会显式调用 this.request.toJSON()

完整方法速查表

成员 类型 说明 app.proxy 影响
request.header / request.headers get/set 请求头对象整体读写
request.get(field) 方法 大小写不敏感地读取单个头(referer/referrer 互通)
request.method get/set 请求方法,可改写以支持 methodOverride
request.url / request.originalUrl get/set / get 当前 URL / 进入时的原始 URL
request.origin get origin 请求头(或 null
request.href get 拼装 protocol://host + originalUrl 的完整 URL 是(host/protocol)
request.path get/set 路径名,setter 保留查询串
request.querystring get/set 不含 ? 的查询串
request.search get/set ? 的查询串,setter 等价 querystring=
request.query get/set 解析后的查询对象(不支持嵌套),带缓存
request.length get Content-Length 的数值或 undefined
request.type get Content-Type 去掉参数
request.charset get Content-Type 的 charset 参数或 ''
request.fresh / stale get GET/HEAD + 2xx/304 下的缓存新鲜度协商
request.protocol get http/https,代理下信任 X-Forwarded-Proto
request.secure get 是否 HTTPS 的布尔简写
request.ip get/set 远端地址,优先 ips[0]
request.ips get IP 链数组(上游→下游),支持 maxIpsCount 截断
request.host get hostname:port,代理下信任 X-Forwarded-Host
request.hostname get 去掉端口,IPv6 委托 WHATWG URL 解析
request.URL get 缓存化的 WHATWG URL 对象
request.subdomains get 子域数组,受 app.subdomainOffset 控制 否(受 hostname)
request.idempotent get 是否幂等(GET/HEAD/PUT/DELETE/OPTIONS/TRACE)
request.socket get 底层 socket
request.is(...types) 方法 Content-Type 匹配判断(null/false/命中的类型)
request.accepts(...) 方法 Accept 协商,返回最佳匹配或 false
request.acceptsEncodings(...) 方法 Accept-Encoding 协商
request.acceptsCharsets(...) 方法 Accept-Charset 协商
request.acceptsLanguages(...) 方法 Accept-Language 协商
request.toJSON() / inspect() 方法 提取 method/url/header 的调试表示

需要深入验证行为的读者,建议直接阅读 lib/request.js 各 getter/setter 的实现,并结合 tests/request/ 下与属性一一对应的测试文件(如 host.test.jsips.test.jsquery.test.jsfresh.test.jsaccept.test.jsis.test.js)亲手运行 npm test 观察结果。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389