LaravelとhtmxでDELETEメソッドを実装したときの罠

に公開

背景

Laravelを使用してCRUDサービスの開発を進める中で、とある課題に直面しました。このプロジェクトでは、バックエンドにLaravelを採用し、フロントエンドのテンプレートエンジンとしてBladeを利用しています。ただし、Bladeだけでは動的なページ表現に限界があるため、Alpine.jsやhtmxを導入して、より柔軟でインタラクティブなUIの実現を図っています。

問題

問題が発生したのは、テーブルのレコードを削除する処理においてです。
レコード削除時には、<form>要素に hx-delete を指定し、DELETEメソッドを使用するように実装しました。

<form hx-delete="/sample/item" hx-target="#element" hx-swap="innerHTML">
    ...
</form>

このフォームを送信すると、ルーティングによって SampleController の destroyItem() メソッドが呼び出され、該当レコードが削除される想定でした。

しかし実際には、destroyItem() に加えて、別のメソッドである destroy() も同時に実行されてしまい、意図しない挙動が発生しました。

特定

当該のdestroy()の実装は概ね以下の通りでした。

public function destroyItem(): RedirectResponse {
    try {
        // Serviceを呼び出し、DELETE操作が実装されている

        return redirect()->route('sample.show');
    } catch (Exception $e) {
        throw $e;
    }
}

web.phpでは以下のようなルーティングが実装されています。

Route::get('/sample', [SampleController::class, 'show'])->name('sample.show');

Route::delete('/sample', [SampleController::class, 'destroy'])->name('sample.destroy');

Route::delete('/sample/item', [SampleController::class, 'destroyItem'])->name('sample.destroy_item');

この問題が発生した原因は、destroyItem() メソッド内でリダイレクトを行っていた点にありました。
hx-delete を使用した場合、コントローラ内でリダイレクトしても、そのリダイレクトは必ずしも GET メソッドで行われるとは限りません。
今回のケースでは、リダイレクトが DELETE メソッドのままで実行されており、結果として DELETE route('sample.show') に遷移してしまいました。
そのため、意図せず DELETE /sample が呼び出され、SampleController の destroy() メソッドが実行されるという予期しない動作が発生していました。

解決

そこでリダイレクト時にGETメソッドで動作するように変更しました。

1. 303を指定する
この問題は、リダイレクト時のステータスコードを 303 See Other に設定することで解決できます。
HTTPステータスコード 303 を使用すると、リダイレクト先へのリクエストは常に GET メソッド で実行されるため、hx-delete による DELETE リクエストのまま遷移することを防げます。

public function destroyItem(): RedirectResponse {
    try {
        // Serviceを呼び出し、DELETE操作が実装されている

        return redirect()->route('sample.show')->setStatusCode(303);
    } catch (Exception $e) {
        throw $e;
    }
}

2. HX-LOCATIONを指定する
もう一つの解決策として、destroyItem() メソッド内で HX-Location ヘッダー を指定したレスポンスを返す方法があります。
これにより、クライアント側では GET メソッドによる遷移が行われ、DELETE メソッドのままリダイレクトされる問題を回避できます。
ただしこの方法では、コントローラー側で次に遷移すべきURLやビューの構造を指定する必要があり、Viewに関する情報をControllerが持つことになります。

今後の学び

そもそもCRUD処理のあとはリダイレクトさせるべきではないのかも?

参照

https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Status/303
https://htmx.org/headers/hx-location/
https://www.reddit.com/r/htmx/comments/rwowfa/django_and_hxdelete_request_verb/
https://github.com/bigskysoftware/htmx/issues/2777

Discussion