Open4

tRPC DeepDive

is_ryois_ryo

このスクラップの目的

tRPCについてもっと深掘りをしていきたいので、ここに雑に書いていく。
ある程度まとまったら記事にでもまとめようかと考え中。

is_ryois_ryo

エラーハンドリング

どういうエラーハンドリングができるのか、Client側(React)、Server側(Express)で調べてみる。

Server

Server側では単純に throw new Error('error') と書いてPromiseをRejectすれば、Client側にエラーレスポンスが返る。

router
export const appRouter = router({
  errorQuery: publicProcedure.query(() => {
    const isError = true;

    if (isError) {
      throw new Error('error');
    }

    return { message: 'ok' };
  }),
});
response
[
    {
        "error": {
            "message": "error",
            "code": -32603,
            "data": {
                "code": "INTERNAL_SERVER_ERROR",
                "httpStatus": 500,
                "stack": "Error: error\n    at ...(省略)",
                "path": "errorQuery"
            }
        }
    }
]

単純にErrorObjectを返すとInternalServerErrorとして扱われる。
ちゃんとエラーコードやステータスを分けたい場合は TRPCError を使えば良い。

router
export const appRouter = router({
  errorQuery: publicProcedure.query(() => {
    const isError = true;

    if (isError) {
      throw new TRPCError({
        message: 'error',
        code: 'BAD_REQUEST',
      });
    }

    return { message: 'ok' };
  }),
});
response
[
    {
        "error": {
            "message": "error",
            "code": -32600,
            "data": {
                "code": "BAD_REQUEST",
                "httpStatus": 400,
                "stack": "TRPCError: error\n    at...(省略),
                "path": "errorQuery"
            }
        }
    }
]

TRPCErrorcode には受け付ける値の型情報があるので、そこから適切なエラーコードを選択してやればClient側にエラーコードやステータスを渡すことができる。
https://trpc.io/docs/server/error-handling

Client

Client側では useQuery を使う場合と useMtation を使う場合でエラーハンドリングのやり方が変わる。
useQuery は返り値の中の error にエラーレスポンスが入ってくる。

useQuery
export const Index = () => {
  const errorQuery = trpc.errorQuery.useQuery();

  if (errorQuery.error) {
    console.log('Error', error.error);
  }

  return <div>index page</div>
}
errorQuery.error
[
    {
        "error": {
            "message": "error",
            "code": -32600,
            "data": {
                "code": "BAD_REQUEST",
                "httpStatus": 400,
                "stack": "TRPCError: error\n    at...(省略),
                "path": "errorQuery"
            }
        }
    }
]

useMutationonError でエラーハンドリングできる。

useMutation
export const Index = () => {
  const error = trpc.errorMutatation.useMutation({
    onError: (e) => {
      console.log('Error', e);
    },
  });

  return <div>index page</div>
}
useMutation.error
[
    {
        "error": {
            "message": "error",
            "code": -32600,
            "data": {
                "code": "BAD_REQUEST",
                "httpStatus": 400,
                "stack": "TRPCError: error\n    at ...(省略),
                "path": "errorMutatation"
            }
        }
    }
]
is_ryois_ryo

エラーハンドリングしてて気になったこと

useQuery でエラーが返ってくるときリトライしてそう。

@tanstack/react-query でQueryClientを生成するときにリトライについての設定ができる。
数値を入れればリトライ回数を指定できて、falseにすればリトライしなくなる。デフォルトは3っぽい。
falseにしてリトライしないようにすると、Network上では失敗とはならないので、GraphQLのエラーだけどエラーレスポンスとして扱ってないという状態になる。

QueryClient
const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            retry: 2,
          },
        },
      }),
);
is_ryois_ryo

認証をしたい

ここでは一旦Tokenでの認証について、どういう方法があるのかを調べる。

Client

Client側は簡単で、trpc.createClient をするときにカスタムヘッダーを設定することができる。
https://trpc.io/docs/client/headers

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として返すようにしている。

context.ts
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の検査ができるようになる。

trpc.ts
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 は認証が必要なエンドポイントという形になる。

appRouter.ts
export const appRouter = router({
  public: publicProcedure.query(() => 'public'),
  auth: authProcedure.query(() => 'auth')
})