chundev
日期:2026-05-23
tRPC 的設計把 HTTP 當實作細節、把 SDK 當主角——所以直接看 wire format 會覺得「URL 看不懂」。但底下就是 HTTP,理解三層 wrap(batch / input index / SuperJSON)+ cookie-based session,就能用 curl 觸發任何 procedure,包括 protectedProcedure。protectedProcedure 不是「URL 鎖起來」,它是 server middleware;session 從 request cookie 還原,curl 帶 cookie jar 就能通過。
在 React app 裡 tRPC SDK 介面很乾淨:
const m = api.cert.generateCsr.useMutation();
m.mutate({ commonName: "example.local", sans: [...] });
但翻 DevTools Network 面板會看到:
POST /api/trpc/cert.generateCsr?batch=1
Body: [{"0":{"json":{"commonName":"example.local","sans":[...]}}}]
Response: [{"result":{"data":{"json":{"jobId":"abc-123"}}}}]
或 query:
GET /api/trpc/cert.history,cert.status?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%2C%22meta%22%3A%7B%22values%22%3A%5B%22undefined%22%5D%2C%22v%22%3A1%7D%7D...
兩件事違反直覺:URL 帶逗號分隔多個 procedure、input 是多層包裝的 URL-encoded JSON。這篇講怎麼讀懂、怎麼用 curl 重現一次 protectedProcedure 呼叫。
tRPC HTTP layer 的設計考量是「給 SDK 用、給 batch 友善、給 SuperJSON 友善」,不是給人讀的。
預設 batch link 啟用,多個 procedure 用逗號接在 URL:
GET /api/trpc/proc1,proc2,proc3?batch=1
即使只呼叫一支也會帶 ?batch=1(fetcher 一律走 batched)。手動 curl 時不加 ?batch=1 是常見錯誤,server 認不出 endpoint。
{"0": {"json": <input for proc1>}, "1": {"json": <input for proc2>}}
0 / 1 對應 URL 裡 procedure 的順序。即使只一個 procedure 也要寫 {"0": ...}。
{"json": value, "meta": {...}}原生 JSON 沒法表達 Date、BigInt、Map、Set、undefined,所以 tRPC 預設用 SuperJSON 把這些型別還原資訊塞進 meta:
{"json": null, "meta": {"values": ["undefined"], "v": 1}}
這個 meta.values = ["undefined"] 告訴 deserializer「上面那個 null 其實要還原成 undefined」。對於 Date:
{"json": "2026-05-23T01:59:18.264Z", "meta": {"values": ["Date"], "v": 1}}
純 JSON 那個 "2026-..." 是字串、meta 告訴對方還原成 Date 物件。多數時候單純 primitive 不需要 meta,但寫 curl 觸發接受複合型別的 procedure 時必須把這個 wrap 做完整。
Response 也是 array、也是 SuperJSON:
[
{"result": {"data": {"json": {"jobId": "abc-123"}}}},
{"result": {"data": {"json": {"status": "ok"}}}}
]
Index 對應 request 的 procedure 順序。
tRPC 的 protected 不在 URL 層、不在 HTTP method 層、不在 IP 白名單層——而在 server middleware:
export const protectedProcedure = publicProcedure.use(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { ...ctx, session: ctx.session } });
});
ctx.session 從哪來?看 tRPC context builder:
export async function createContext({ headers, req }) {
const session = await auth.api.getSession({ headers }); // ← 從 cookie 讀
return { db, session, headers };
}
整條鏈:
HTTP cookie ─→ auth.api.getSession() ─→ ctx.session ─→ protectedProcedure middleware ─→ procedure
URL 沒鎖、沒 token in URL——一切靠 server 端 middleware 驗 session。Client 端就算掃到 procedure 名稱,沒帶 cookie 也只會收到:
{"error": {"json": {"message": "UNAUTHORIZED", "code": -32001, ...}}}
許多現代 JS 認證庫(例如 better-auth)用 HttpOnly + Secure + SameSite 的 cookie 存 session token:
Set-Cookie: __Secure-better-auth.session_token=...; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000
curl 用 cookie jar 把它存下來、後續請求自動帶上:
# 1. 登入,把 set-cookie 寫進 jar
curl -X POST https://app.example.com/api/auth/sign-in/username \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"..."}' \
-c /tmp/cookies.txt
# 2. mutation = POST,body 帶完整三層 wrap
curl -X POST 'https://app.example.com/api/trpc/cert.generateCsr?batch=1' \
-H 'Content-Type: application/json' \
-b /tmp/cookies.txt \
-d '{"0":{"json":{"commonName":"foo.example.local","sans":["foo.example.local"]}}}'
# Response: [{"result":{"data":{"json":{"jobId":"abc-123"}}}}]
# 3. query = GET,input 進 URL(URL-encoded JSON)
INPUT=$(python3 -c '
import json, urllib.parse
print(urllib.parse.quote(json.dumps({"0":{"json":{"jobId":"abc-123"}}})))
')
curl 'https://app.example.com/api/trpc/cert.jobStatus?batch=1&input='"$INPUT" \
-b /tmp/cookies.txt
-c 寫 jar、-b 讀 jar。如果 server 用自簽憑證再加 -k(測試環境常見)。
也得帶 {"0":{"json":null,"meta":{"values":["undefined"],"v":1}}}——這是 undefined 的 SuperJSON 編碼。漏掉 meta 有些 procedure 會收到 null 而不是 undefined,對 Zod schema 的 optional() 跟 nullable() 有差。
雖然有 SDK 為什麼還要 curl?實務上至少四個場景:
TRPCClientError 看不出原始 body,curl 可以印 raw HTTPprotectedProcedure 不是 URL 鎖——是 server middleware 在 procedure 之前跑。Cookie session 是唯一的鑰匙;client 沒法繞過 server 端檢查。{"0":{"json":...}} 的形狀。protectedProcedure。對 debugging、運維、整合測試是有用的逃生門。meta 不是裝飾——如果 input/output 包 Date / BigInt / undefined / Map / Set 等 plain JSON 沒法表達的型別,少了 meta 就 round-trip 失敗。{json, meta}protectedProcedure 的標準 patternauth.api.getSession() 怎麼從 headers/cookies 還原 session