H3 HTTP QUERY 方法实战
有时你需要一个「GET 语义」的接口,但查询条件太复杂、太长,塞不进 URL。HTTP QUERY 方法(RFC 10008)就是为这个场景生的:它和 GET 一样安全、幂等、可缓存,但允许在请求体里带一个带 Content-Type 的查询。
H3 把 QUERY 当成一等公民支持:既有 app.query() 注册入口,也有两个配套工具。
注册 QUERY handler
写法和 POST 几乎一样,只是方法名换成 query:
import { readBody } from "h3";
// QUERY 携带的请求体,读取方式和 POST 完全一致
app.query("/books", async (event) => {
const query = await readBody(event, { type: "text" }); // 按纯文本读,自行解析
return runSearch(query); // 跑你的检索逻辑
});
⚠️ 因为
QUERY同样允许客户端控制请求体,body-size 限制一样生效。和 POST 一样,别忘了用assertBodySize兜底,否则一个超大 body 能拖垮服务。
声明接受的格式:Accept-Query
QUERY 的语义要求服务端告诉客户端「我懂哪些查询格式」。用 appendAcceptQuery 在响应头里写 Accept-Query(一个 Structured Fields 列表)。它也能挂在普通的 GET 上,让客户端在发 QUERY 之前就能发现支持的格式:
import { appendAcceptQuery } from "h3";
app.get("/books", (event) => {
appendAcceptQuery(event, ["application/sql", "application/jsonpath"]);
// 响应头变成:Accept-Query: application/sql, application/jsonpath
return "可用 SQL 或 JSONPath 作为 QUERY 请求体";
});
校验 Content-Type:400 / 415 / 422
requireContentType 严格按 RFC 的错误语义工作:缺 Content-Type 抛 400、不支持抛 415、格式坏了抛 422。三个状态码正好对应三种不同的失败原因,客户端能据此给出精准提示:
import { requireContentType, readBody } from "h3";
app.query("/books", async (event) => {
// 先校验类型,再读体;类型不对会在这里直接抛错
const type = requireContentType(event, ["application/sql", "application/jsonpath"]);
const query = await readBody(event, { type: "text" });
return runQuery(type, query); // type 是命中的那个媒体类型
});
提供一个可缓存的 GET 替代
QUERY 的响应没有 URL 可以寻址,浏览器和 CDN 没法缓存它。RFC 10008 建议用 Content-Location 头把客户端指到一个等价的、可缓存的 GET 上:把查询结果用一个稳定 id 存起来,客户端就能用普通 GET 重复取:
app.query("/books", async (event) => {
const result = runQuery(type, query);
const id = queryId(type, query); // 对查询算一个稳定哈希作为 id
cache.set(id, result); // 存进你的缓存层
event.res.headers.set("content-location", `/books/${id}`);
return result;
});
完整示例(逐行注释)
下面这个 /books 资源同时支持 SQL 和 JSONPath 两种查询体,声明可接受格式、校验类型,并在响应里给出可缓存的 GET 入口:
import { appendAcceptQuery, requireContentType, readBody } from "h3";
const cache = new Map<string, unknown>(); // 简化版缓存,生产请换真缓存
app.query("/books", async (event) => {
// 1) 先声明本资源理解的查询格式,方便客户端发现
appendAcceptQuery(event, ["application/sql", "application/jsonpath"]);
// 2) 校验请求体类型:缺失→400,不支持→415,畸形→422
const type = requireContentType(event, ["application/sql", "application/jsonpath"]);
// 3) 按纯文本读取请求体(示例里自行解析,也可用 type: "json")
const query = await readBody(event, { type: "text" });
// 4) 执行查询
const result = runQuery(type, query);
// 5) 把结果存到稳定 id 下,并告诉客户端可用 GET 重复获取
const id = queryId(type, query);
cache.set(id, result);
event.res.headers.set("content-location", `/books/${id}`);
return result;
});
// 下面两个辅助函数由你的业务提供
declare function runQuery(type: string, query: string): unknown;
declare function queryId(type: string, query: string): string;
什么时候该用 QUERY,而不是 GET + 请求体
有人会问:我直接 POST 一个查询体不行吗?区别在于语义和缓存:
| 方式 | 语义 | 可缓存 | 幂等 | 适用 |
|---|---|---|---|---|
| GET + URL 参数 | 安全 | 是(URL 可寻址) | 是 | 查询短小、能放 URL |
| POST + body | 非安全 | 否(默认) | 否 | 写操作、或查询太长但不想被缓存 |
| QUERY + body | 安全 | 否(但可用 Content-Location 指向 GET) |
是 | 查询太长 / 太结构化,但仍希望是「只读查询」语义 |
QUERY 的甜区正是「查询太大塞不进 URL,但它本质仍是只读查询」——这时候用 POST 会丢失「安全 / 幂等」的语义承诺,而 QUERY 两全。
用 curl 调用
## 发一个 QUERY 请求,携带 SQL 查询体
curl -X QUERY http://localhost:3000/books \
-H "Content-Type: application/sql" \
-d "SELECT * FROM books WHERE price < 50"
## 类型不对会被 415 拒绝
curl -X QUERY http://localhost:3000/books \
-H "Content-Type: application/xml" \
-d "<q/>"
⚠️ 和 GET 不同,
QUERY不在 CORS 安全列表里,浏览器会先发一次预检(OPTIONS)。如果你用handleCors时显式传了methods白名单,务必把"QUERY"加进去,否则跨域调用会在预检阶段就被拦。