tRPC DeepDive
このスクラップの目的
tRPCについてもっと深掘りをしていきたいので、ここに雑に書いていく。
ある程度まとまったら記事にでもまとめようかと考え中。
エラーハンドリング
どういうエラーハンドリングができるのか、Client側(React)、Server側(Express)で調べてみる。
Server
Server側では単純に throw new Error('error')
と書いてPromiseをRejectすれば、Client側にエラーレスポンスが返る。
export const appRouter = router({
errorQuery: publicProcedure.query(() => {
const isError = true;
if (isError) {
throw new Error('error');
}
return { message: 'ok' };
}),
});
[
{
"error": {
"message": "error",
"code": -32603,
"data": {
"code": "INTERNAL_SERVER_ERROR",
"httpStatus": 500,
"stack": "Error: error\n at ...(省略)",
"path": "errorQuery"
}
}
}
]
単純にErrorObjectを返すとInternalServerErrorとして扱われる。
ちゃんとエラーコードやステータスを分けたい場合は TRPCError
を使えば良い。
export const appRouter = router({
errorQuery: publicProcedure.query(() => {
const isError = true;
if (isError) {
throw new TRPCError({
message: 'error',
code: 'BAD_REQUEST',
});
}
return { message: 'ok' };
}),
});
[
{
"error": {
"message": "error",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"stack": "TRPCError: error\n at...(省略),
"path": "errorQuery"
}
}
}
]
TRPCError
の code
には受け付ける値の型情報があるので、そこから適切なエラーコードを選択してやればClient側にエラーコードやステータスを渡すことができる。
Client
Client側では useQuery
を使う場合と useMtation
を使う場合でエラーハンドリングのやり方が変わる。
useQuery
は返り値の中の error
にエラーレスポンスが入ってくる。
export const Index = () => {
const errorQuery = trpc.errorQuery.useQuery();
if (errorQuery.error) {
console.log('Error', error.error);
}
return <div>index page</div>
}
[
{
"error": {
"message": "error",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"stack": "TRPCError: error\n at...(省略),
"path": "errorQuery"
}
}
}
]
useMutation
は onError
でエラーハンドリングできる。
export const Index = () => {
const error = trpc.errorMutatation.useMutation({
onError: (e) => {
console.log('Error', e);
},
});
return <div>index page</div>
}
[
{
"error": {
"message": "error",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"stack": "TRPCError: error\n at ...(省略),
"path": "errorMutatation"
}
}
}
]
エラーハンドリングしてて気になったこと
useQuery
でエラーが返ってくるときリトライしてそう。
@tanstack/react-query
でQueryClientを生成するときにリトライについての設定ができる。
数値を入れればリトライ回数を指定できて、falseにすればリトライしなくなる。デフォルトは3っぽい。
falseにしてリトライしないようにすると、Network上では失敗とはならないので、GraphQLのエラーだけどエラーレスポンスとして扱ってないという状態になる。
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: 2,
},
},
}),
);
認証をしたい
ここでは一旦Tokenでの認証について、どういう方法があるのかを調べる。
Client
Client側は簡単で、trpc.createClient
をするときにカスタムヘッダーを設定することができる。
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3001/trpc',
headers: {
authorization: 'Bearer token',
},
}),
],
}),
);
Server
middlewareを使ってContextにヘッダーを渡して、procedure内で判定することができる。
まずmiddlewareでContextを作成する。
下記では受け取ったrequestからheadersを取り出してContextとして返すようにしている。
import * as trpcExpress from '@trpc/server/adapters/express';
export function createContext({
req,
res,
}: trpcExpress.CreateExpressContextOptions) {
return {
headers: req.headers,
req,
res,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;
tRPCをinitする段階でContextを利用する。
そうするとprocedureで引数からContextが取得できて、その中のheadersからTokenの検査ができるようになる。
import { TRPCError, initTRPC } from '@trpc/server';
import { Context } from './context';
const t = initTRPC.context<Context>().create();
export const { router } = t;
export const publicProcedure = t.procedure;
export const authProcedure = publicProcedure.use((opts) => {
const { ctx } = opts;
let isAuthed = false;
if (ctx.headers.authorization) {
isAuthed = ctx.headers.authorization.split(' ')[1] === 'token';
}
if (!isAuthed) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx,
});
});
あとは authProcedure
をRouterで利用するだけ。
こうすると publicProcedure
は認証がいらないエンドポイント、authProcedure
は認証が必要なエンドポイントという形になる。
export const appRouter = router({
public: publicProcedure.query(() => 'public'),
auth: authProcedure.query(() => 'auth')
})