4.1 请求拦截:匹配规则与读取请求
本章解决两个问题:让 handler 精确命中目标请求,以及从请求里取出路径参数、查询串、请求头、Cookie 和表单。
路径匹配
三种 predicate
// 1) 字符串(精确路径,可用通配源 *)
http.get("*/api/users", listUsers);
// 2) 路径模板(Path-to-RegExp 风格,:param 命名参数)
http.get("*/api/users/:userId", getUser);
http.get("*/api/users/:userId/posts/:postId", getPost);
// 3) 正则(应对动态性最强的场景)
http.get(/\/api\/users\/\w+\/avatar$/, getAvatar);
// 通配 *:匹配任意层级
http.get("*/api/files/*", anyFile);源(origin)部分:
*匹配任意源,浏览器与 Node 通用,本教程一律推荐;- 写死
https://api.example.com/users可严格限定源; - 相对路径
/users只在浏览器有效(Node 无当前 origin)。
匹配只针对路径与源,查询参数(?q=...)不参与匹配 —— 查询串在 resolver 里解析。
路径参数
http.get("*/api/users/:userId", ({ params }) => {
const userId = params.userId; // 注意:开启 noUncheckedIndexedAccess 时类型是 string | undefined
// ...
});查询参数
http.get("*/api/users", ({ request }) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get("page") ?? 1);
const pageSize = Number(url.searchParams.get("pageSize") ?? 20);
const q = url.searchParams.get("q") ?? ""; // 全文搜索
const role = url.searchParams.get("role"); // ?role=admin
// ...
});亲手试:下面两个请求只差查询串,返回结果完全不同(模拟数据库里有 24 个用户)。
查询参数:分页 + 搜索
换个关键字
请求方法与请求体
http.post("*/api/posts", async ({ request }) => {
// JSON
const body = (await request.json()) as CreatePostInput;
// 文本
const raw = await request.text();
// FormData(multipart 上传)
const form = await request.formData();
const file = form.get("file"); // File | null —— 字段名约定为 file
});请求体只读取一次(Request.body 是流);需要多次读取时先 await request.clone().json()。
FormData 提交
本教程的上传 handler 只认字段名 file,缺字段返回 422:
http.post("*/api/upload", async ({ request }) => {
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return HttpResponse.json(
{ error: { code: "MISSING_FILE", message: "multipart 字段 file 必填" } },
{ status: 422 },
);
}
return HttpResponse.json(
{ id: crypto.randomUUID(), filename: file.name, mediaType: file.type, size: file.size },
{ status: 201 },
);
});浏览器
fetch传FormData时不要手动设置Content-Type,浏览器会自动带上 boundary。
请求头校验
http.get("*/api/require-header", ({ request }) => {
const requestId = request.headers.get("x-request-id");
if (!requestId) {
return HttpResponse.json(
{ error: { code: "REQUEST_ID_REQUIRED" } },
{ status: 400 },
);
}
const accept = request.headers.get("accept") ?? "";
if (!accept.includes("application/json") && !accept.includes("*/*")) {
return HttpResponse.json(
{ error: { code: "NOT_ACCEPTABLE" } },
{ status: 406 },
);
}
return HttpResponse.json({ echoed: requestId });
});用控制台分别发"带头"和"不带头"的请求对比:
请求头校验(400 / 406 / 200)
把请求头改成 {} 再发 → 400;保留 x-request-id 但把 Accept 改成 text/plain → 406。
Cookie
http.get("*/api/me", ({ cookies }) => {
const session = cookies["session_id"];
if (!session) {
const res = HttpResponse.json({ error: { code: "NO_SESSION" } }, { status: 401 });
res.headers.append("Set-Cookie", "session_id=; Max-Age=0");
return res;
}
// ...
});CORS 模拟
应用与 API 不同源时,MSW 同样运行在应用页面的 worker 中 —— 请求"看起来"跨域,但响应由同源 worker 合成,浏览器不会发起真实预检。 如需模拟跨域响应头,直接在 HttpResponse 上加:
HttpResponse.json(data, {
headers: {
"Access-Control-Allow-Origin": "https://app.example.com",
"Access-Control-Allow-Credentials": "true",
},
});passthrough:部分放行
真实接口已就绪、只想 mock 个别端点时,让其余请求穿透:
import { http, passthrough } from "msw";
export const handlers = [
http.get("*/api/users/*", () => passthrough()), // 用户接口走真实后端
http.get("*/api/posts", mockPosts), // 文章继续 mock
];本教程示例还演示了对 https://api.github.com/* 的 passthrough —— mock 与真实 API 可以混用。
小练习
写操作需要 Bearer 令牌(下一章会细讲鉴权模拟),所以分两步:
第一步:登录 admin / admin1234,从响应的 tokenPair.accessToken 复制令牌:
① 登录获取令牌
第二步:把令牌粘进请求头,然后尝试:
- 创建用户(body 给
name/email/role)→ 观察 201 + Location; - 用同一个 email 再发一次 → 409 CONFLICT;
- body 改成
{}→ 422 校验错误; - 删掉请求头 → 401。
② 携带 Bearer 令牌创建用户