nginx でトレイリングスラッシュなしの URL に POST するとリクエストボディが欠落する

nginx におけるトレイリングスラッシュの有無と POST リクエストにまつわる話。

目次

発端

nginx サーバの次のような位置に PHP を配置した。

http://example.com/example/index.php

この index.php は、GET リクエストすると普通の HTML を返す。POST リクエストすると、ちゃんとそのデータを受け取ってレスポンスできる、というモノになっている。

/index.php という部分は、nginx の index ディレクティブにより、トレイリングスラッシュ (末尾のスラッシュ) なしでアクセスしても、キチンと /index.php が返るようになっている。

# nginx の設定ファイル
server {
  # …中略…
  
  location / {
    root   /var/www/html;
    index  index.html index.php;  # ← コレのおかげで…
  }
}
# 以下のどれでも同じように GET リクエストできている
$ curl -X GET http://example.com/example/index.php
$ curl -X GET http://example.com/example/
$ curl -X GET http://example.com/example

example/ で終わっているのが「トレイリングスラッシュあり」、example で終わっているのが「トレイリングスラッシュなし」と呼ばれる URL 形式だ。

コレで /index.php を書かずに済むならスッキリするなーと思ったのだが、POST してみてつまづいた。

# /index.php まで指定した場合。リクエストボディを認識しているテイ
$ curl -X POST -d 'my_param=example' http://example.com/example/index.php
Hello [example] !

# トレイリングスラッシュありでも。リクエストボディを認識している
$ curl -X POST -d 'my_param=example' http://example.com/example/
Hello [example] !

# トレイリングスラッシュなし
$ curl -X POST -d 'my_param=example' http://example.com/example
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.16.1</center>
</body>
</html>

お!?

トレイリングスラッシュなしのパターンだけ、301 のリダイレクトが発生している…。

じゃあリダイレクトして確認してみるか、と思うと…

# トレイリングスラッシュなし・リダイレクト (-L) とデバッグ出力 (-vvv) を有効にする
$ curl -X POST -d 'my_param=example' -L -vvv http://example.com/example

# デバッグログ抜粋
< HTTP/1.1 301 Moved Permanently
< Location: http://example.com/example/
* Issue another request to this URL: 'http://example.com/example/'
* Violate RFC 2616/10.3.2 and switch from POST to GET

# GET リクエストのレスポンスが返される

なんと、トレイリングスラッシュありの URL にリダイレクトしつつ、POST メソッドが GET メソッドに変換されているようだ。コレでは POST リクエストが通らないだろう。

トレイリングスラッシュ「なし」から「あり」にリダイレクトする挙動は元々の仕様らしいが、その中に「POST リクエストだった場合は GET リクエストに変換する」という動きがあるようだ。

解決策

ドンズバな解決策がネット上では見つからず、自分で編み出した解決策を紹介する。

# nginx の設定ファイル
server {
  # …中略… (今までの設定部分はそのまま)
  
  # トレイリングスラッシュなしで POST リクエストされる URL パスを指定する
  location ~ ^/example$ {
    return  307  $scheme://$host/example/index.php?$args;
  }
}

HTTP 307 で /index.php まで付与した URL をレスポンスするように指定している。

http でも https でも認識するように $scheme 変数を使い、Public IP 直打ちでも独自ドメインでも CORS 制約に引っかからないようにするために $host 変数を使っている。$args はリクエストパラメータが付いていた場合の制御だが、なくても大丈夫かと。

POST リクエストを処理したい URL ごとにコレを用意しないといけなくて、ちょっと微妙な気がしているが、そんなに大量の設定を nginx で持たないように、nginx はプロキシ的に軽く使うのが本来は推奨なんだろうなーと思った。

nginx の設定ファイルはまだまだ勉強していかないとよく分からないぞー。