1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

axumでMultipartを使うとHandlerが実装されない?(AI時代に求められる資質)

1
Posted at

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

たとえば以下です。

  • State
  • Path
  • Query
  • Header

これらは、URL やヘッダなどの情報を読むだけです。
request body そのものには触れません。

2. body を読む extractor

たとえば以下です。

  • Json
  • Multipart
  • Form
  • Bytes
  • String

これらは 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 の並びを疑ったほうがいいです。

特に次のような型を使っているなら要注意です。

  • Json
  • Multipart
  • Form
  • Bytes
  • String

これらは body を読むので、最後に置く必要があります。

順番のイメージとしては、だいたいこんな感じです。

Path
Query
State
Header
↓
Json / Multipart / Form / Bytes

まとめ

Multipart を最後に置く

つまり、

State → Multipart

の順番にする!


最後に: AIが苦手な分野と、これからのプログラマーに必要なこと

今回おもしろかったのは、AIがそれっぽい原因をいくつも出してくるのに、本当の原因には届かなかったことです。

今回のケースでは、AIは次のような点を苦手としていると感じました。

  • ライブラリ固有の細かいルールを、文脈込みで正確に絞り込むこと
  • エラーメッセージが本質を隠しているときに、最短で切り分けること
  • 一般論ではなく、その場の最小再現から本当の原因を掘り当てること

つまりAIは、広くそれっぽい候補を出すのは得意ですが、
泥臭い切り分けや違和感のあるエラーの現場対応はまだ弱いです。

だからこそ、これからのプログラマーには次の資質がより重要になると思います。

  • エラーをうのみにせず、最小構成で切り分ける力
  • 「本当にそこが原因か?」と疑い続ける力
  • AIの回答を鵜呑みにせず、検証しながら使う力
  • ライブラリやフレームワークのルールを一次情報で確認する姿勢

AIが使える時代になったから、実装力がいらなくなるのではなく、
むしろ 曖昧な情報の中から本当の原因を見つけるデバッグ力 がいっそう大事になるのだと思います。

今回の件は、そのことをかなり実感した体験でした。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?