axumで「Handlerが実装されていない」と言われた。
axumでファイルアップロード用の handler を書いていたとき、かなり分かりにくいエラーにハマりました。
書いていたのはこんなコードです。
pub async fn upload_handler<Saver, DB>(
multipart: Multipart,
State(state): State<EngineState<Saver, DB>>,
) -> impl IntoResponse
{
"ok"
}
Router::new().route("/upload", post(upload_handler::<Saver, DB>))
これをコンパイルすると、こんなエラーが出ます。
the trait Handler<_, _> is not implemented
つまり、post に渡す引数としてこの関数は受け取れない、ということです。
見た感じは普通の handler です。
async fn-
impl IntoResponseを返している - extractor も使っている
それなのに、なぜか Handler にならない。
かなり意味が分かりませんでした。
なぜ原因が見えにくいのか
axum は、普通の関数を handler として扱えるように、内部でかなりうまく Handler トレイトを実装しています。
この仕組みのおかげで普段はとても楽なのですが、逆に「なぜ handler として認識されないのか」は一気に追いづらくなります。
しかも今回のように generics を使った handler だと、axum が用意している axum::debug_handler もそのままでは使えません。
そのため、エラーの本当の原因をスクリプト単体で切り分けるのがかなり難しくなります。
AIに聞いてみた
そこでAIにも聞いてみました。
Claude CLI
「ジェネリクス関数はHandlerにならないからです」
Claude Web
「EngineState<Saver, DB> が Send である保証が where 句にないからです」
ChatGPT
「upload_handler の Future が Send になっていません」
それっぽい説明だったので全部試したのですが、何も解決しませんでした。
結局、これで3時間くらい悩みました。
解決の瞬間
さすがに原因が分からなすぎたので、一度問題を極限まで単純化してみることにしました。
ジェネリクスも消す。
Stateも最小にする。
とにかく 一番シンプルな handler を作ってみる。
async fn handler(multipart: Multipart, State(state): State<Arc<()>>) -> impl IntoResponse {
"ok"
}
そしてコンパイル。
すると今まで見たことのないエラーが出ました。
error: `Multipart` consumes the request body and thus must be the last argument to the handler function
その瞬間、「あっ」となりました。
今までのエラーはずっと
Handler trait not implemented
でした。
だから
- trait境界の問題?
- Send?
- generics?
とずっとそっちを疑っていたんです。
でも、このエラーは全然違うことを言っていました。
Multipart は request body を消費するので、handler の最後の引数でなければならない
そこで、元のコードを試しに順番を入れ替えてみると
pub async fn upload_handler<Saver, DB>(
State(state): State<EngineState<Saver, DB>>,
multipart: Multipart,
) -> impl IntoResponse
{
"ok"
}
コンパイル。
通りました。
たったそれだけでした。
なぜこんなことが起きるのか
axum の extractor には、ざっくり分けて2種類あります。
1. body を読まない extractor
たとえば以下です。
StatePathQueryHeader
これらは、URL やヘッダなどの情報を読むだけです。
request body そのものには触れません。
2. body を読む extractor
たとえば以下です。
JsonMultipartFormBytesString
これらは HTTP リクエストの body を読み取ります。
HTTPのbodyは一回しか読めない
HTTP リクエストはざっくりこんな構造です。
request
├ headers
├ path
├ query
└ body
このうち body はストリームなので、基本的に一度しか読めません。
だから axum では、
body を読む extractor は最後に置く
というルールがあります。
今回のコードをこのルールで見ると
ダメだったのはこれです。
Multipart
State
Multipart が先に body を取りにいってしまうので、その後ろに extractor を置けません。
一方で、正しいのはこれです。
State
Multipart
body を読む extractor が最後に来ているので問題ありません。
axumでハマったときの確認ポイント
Handler trait not implemented のようなエラーが出たときは、trait 実装そのものより先に、まず extractor の並びを疑ったほうがいいです。
特に次のような型を使っているなら要注意です。
JsonMultipartFormBytesString
これらは body を読むので、最後に置く必要があります。
順番のイメージとしては、だいたいこんな感じです。
Path
Query
State
Header
↓
Json / Multipart / Form / Bytes
まとめ
Multipart を最後に置く
つまり、
State → Multipart
の順番にする!
最後に: AIが苦手な分野と、これからのプログラマーに必要なこと
今回おもしろかったのは、AIがそれっぽい原因をいくつも出してくるのに、本当の原因には届かなかったことです。
今回のケースでは、AIは次のような点を苦手としていると感じました。
- ライブラリ固有の細かいルールを、文脈込みで正確に絞り込むこと
- エラーメッセージが本質を隠しているときに、最短で切り分けること
- 一般論ではなく、その場の最小再現から本当の原因を掘り当てること
つまりAIは、広くそれっぽい候補を出すのは得意ですが、
泥臭い切り分けや違和感のあるエラーの現場対応はまだ弱いです。
だからこそ、これからのプログラマーには次の資質がより重要になると思います。
- エラーをうのみにせず、最小構成で切り分ける力
- 「本当にそこが原因か?」と疑い続ける力
- AIの回答を鵜呑みにせず、検証しながら使う力
- ライブラリやフレームワークのルールを一次情報で確認する姿勢
AIが使える時代になったから、実装力がいらなくなるのではなく、
むしろ 曖昧な情報の中から本当の原因を見つけるデバッグ力 がいっそう大事になるのだと思います。
今回の件は、そのことをかなり実感した体験でした。