H3 HTTP QUERY 方法实战

2026-10-30 17:48 更新

有时你需要一个「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" 加进去,否则跨域调用会在预检阶段就被拦。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号