2026年11月10日、PythonのWebアプリを本番で動かすときによく使われるWebサーバーGunicornの27.0.0が公開されました。8月の26.2.0から、およそ2か月半ぶりのメジャーバージョンの更新です。
今回の変更履歴には、HTTPの決まりに沿わないリクエストを、これまでより厳しく断るための修正が、ずらりと並んでいます。互換性を壊す変更として、Transfer-Encoding: identityを付けたリクエストを、400のエラーで断るようになりました。
普通のブラウザからのアクセスなら、まず影響はありません。ただ、自作のプログラムや古い機器からAPIを呼んでいる場合は、上げたとたんに400が返るようになるかもしれません。
まずはGunicornとはどんなものか¶
最初に、Gunicornを知らない方のために、どんなソフトなのかを簡単に紹介します。Gunicornは、PythonのWebアプリを、インターネットからのアクセスに耐えられる形で動かすためのWebサーバーです。
DjangoやFlaskには、開発用の簡単なサーバーが付いています。しかし、それは本番で使うためのものではないので、本番ではGunicornのような専用のサーバーを使うのが一般的です。
Gunicornの起動のしかた¶
Gunicornは、ターミナルから1行のコマンドで起動できます。今回の確認でも、次のようにして、app.pyの中のappという関数を動かしました。
gunicorn -b 127.0.0.1:8000 -w 1 app:app
-bはどのアドレスとポートで待ち受けるか、-wはリクエストをさばくワーカーというプロセスをいくつ立てるかの指定です。app:appは、app.pyというファイルの中の、appという名前のアプリを動かす、という意味になります。
Djangoなら、gunicorn mysite.wsgiのように、プロジェクトに用意されているwsgi.pyを指定するのが一般的です。アプリのコードはそのままで、動かし方だけを本番向けに変えられるのが、こうしたサーバーの便利なところです。
WSGIとASGI¶
GunicornとWebアプリは、WSGIという決まりごとでつながっています。WSGIは、WebサーバーとPythonのWebアプリの間で、リクエストと応答をどう受け渡すかを決めたものです。
| 決まり | 読み方 | 主に使うフレームワーク |
|---|---|---|
| WSGI | ウィズギー | Django、Flask |
| ASGI | エーズギー | FastAPI、Starlette、非同期のDjango |
変更履歴を見ると、Gunicornには、WSGIだけでなくASGIのアプリを動かすためのワーカーもあり、今回はその両方に多くの修正が入っています。Flaskの基本は、こちらで紹介しています。【関連記事】PythonのFlaskとは?
27.0で何が変わったのかを整理しよう¶
続いて、27.0.0の主な変更を表にまとめます。内容は、Gunicornの公式リポジトリにある変更履歴docs/content/news.mdにもとづいています。
| 分類 | 主な変更 |
|---|---|
| 互換性を壊す変更 | Transfer-Encodingの最後がchunkedでないリクエストを、400で断る |
| リクエストの検査 | パスの制御文字や、そのままの日本語などの文字を断る |
| リクエストの検査 | 長すぎるリクエスト行に、414 URI Too Longを返す |
| ASGI | 応答の長さや、WebSocketの扱いを、決まりどおりに直す |
| 速度 | 大きなリクエストの本文を、まとめて読むように |
変更履歴の修正の多くには、RFC 9112やRFC 9110といった番号が添えられています。RFCは、インターネットの決まりごとをまとめた文書で、これらはHTTPの決まりを定めたものです。
PyPIの記録によると、27.0.0が必要とするPythonは3.10以上です。
26.2.0と27.0.0に同じリクエストを送ってみた¶
では、実際に試してみましょう。受け取った本文の長さを返すだけの小さなWSGIアプリを用意し、2つの版のGunicornで動かしました。
def app(environ, start_response):
body = environ["wsgi.input"].read()
msg = f"受け取った本文: {len(body)}バイト\n".encode()
start_response("200 OK", [
("Content-Type", "text/plain; charset=utf-8"),
("Content-Length", str(len(msg))),
])
return [msg]
ブラウザでは送れないような、少し変わったリクエストも試したいので、Pythonのsocketを使って、HTTPのリクエストを文字で直接組み立てて送りました。
import socket
raw = (
b"POST / HTTP/1.1\r\n"
b"Host: x\r\n"
b"Transfer-Encoding: identity\r\n"
b"Content-Length: 5\r\n"
b"\r\n"
b"hello"
)
s = socket.create_connection(("127.0.0.1", 8000), timeout=3)
s.sendall(raw)
print(s.recv(200).split(b"\r\n")[0])
結果を比べた¶
このようにして、5種類のリクエストを送った結果を表にまとめます。手元のPython 3.11で、それぞれの版を新しい仮想環境に入れて試しました。
| 送ったリクエスト | 26.2.0 | 27.0.0 |
|---|---|---|
| Transfer-Encoding: identity付き | 200 OK(本文5バイトを受け取った) | 400 Bad Request |
パスに日本語をそのまま書いた/商品 |
200 OK | 400 Bad Request |
| パスに制御文字を含む | 200 OK | 400 Bad Request |
| 5,000文字のURL | 400 Bad Request | 414 URI Too Long |
Content-Lengthが+5 |
400 Bad Request | 400 Bad Request |
上の3つは、26.2.0では受け付けられていたものが、27.0.0では400で断られるようになりました。4つ目は、どちらも断っていますが、27.0.0では長すぎるという意味の、より正確な414を返しています。
最後の+5のように、数字の前に記号を付けたContent-Lengthは、どちらの版でも断られました。
Transfer-Encodingの変更とは¶
一番大きな変更である、Transfer-Encodingについて、もう少し詳しく見ていきましょう。Transfer-Encodingは、リクエストの本文を、どんな形で送るかを伝えるヘッダーです。
よく使われるのは、本文を小分けにして送るchunkedです。HTTPの決まりでは、Transfer-Encodingを付けるなら、その最後はchunkedでなければならないとされています。
これまでの動きの問題¶
変更履歴によると、これまでのWSGIのワーカーは、identityのように最後がchunkedでないリクエストの本文を、接続が閉じるまで読み続けていました。一方、ASGIのワーカーは、同じ本文を空として扱っていたそうです。
サーバーによって、どこまでが本文かの解釈が違うのは、危険なことです。前に置いた別のサーバーと解釈がずれると、本文の一部を次のリクエストとして扱わせる、リクエストスマグリングと呼ばれる攻撃につながることがあります。
送る側が直すこと¶
変更履歴には、Transfer-Encoding: identityを送っているクライアントは、送るのをやめる必要がある、と書かれています。本文の長さがわかっているなら、Content-Lengthだけを付ければ十分です。
自作のプログラムでHTTPのヘッダーを組み立てている場合は、上げる前に、送っているヘッダーを確かめておきましょう。
パスの日本語は、パーセント符号化が必要に¶
2つ目の、パスの日本語の変更も、初心者が出会いやすいかもしれません。変更履歴によると、リクエストのパスに、生の日本語などの文字が入っていると断るようになりました。
ブラウザは、URLの日本語を、自動で%E5%95%86のような形に変えてから送ります。これを、パーセント符号化と呼びます。
符号化すれば受け付けられる¶
手元で、/商品をパーセント符号化した/%E5%95%86%E5%93%81を送ると、27.0.0でも200 OKが返ってきました。Pythonでは、urllib.parse.quote()で、この形に変えられます。
from urllib.parse import quote
print(quote("/商品")) # /%E5%95%86%E5%93%81
ブラウザやrequestsのような一般的な道具を使っていれば、自動で符号化されるので、心配はいりません。自分でリクエストの文字を組み立てているときだけ、気をつけましょう。
Pythonで手軽にWebサーバーを立てる方法は、こちらで紹介しています。【関連記事】Pythonのhttp.serverとは?
そのほかの変更¶
変更履歴には、ほかにもたくさんの修正が並んでいます。代表的なものを表にまとめます。
| 修正 | 中身 |
|---|---|
| 大きなアップロード | 本文を8KiBずつではなく、まとめて読むので、大きなアップロードがずっと速く |
| ASGIの起動失敗 | lifespanの起動が失敗したら、Gunicornを止める |
| WebSocket | 新しい設定websocket_max_message_sizeで、大きすぎるメッセージを断る。既定は16MiB |
| 100-continue | curlなどが、アップロードの前に1秒ほど待たされていた問題を修正 |
| 繰り返されたヘッダー | 同じヘッダーがたくさんあるときの処理を、時間がかかりすぎないように |
似たような、リクエストを厳しく確かめる修正は、先日のTornado 6.6でも入っていました。【関連記事】Tornado 6.6がリリース
上げる前にやっておきたいこと¶
最後に、Gunicornを27.0.0へ上げる前に確かめておきたいことを整理します。
| 順番 | 確認すること |
|---|---|
| 1 | 自作のクライアントや機器が、Transfer-Encoding: identityを送っていないか |
| 2 | パスに、パーセント符号化していない文字を入れて送るクライアントがないか |
| 3 | 上げたあと、ログで400や414が急に増えていないか |
| 4 | ステージング環境で、ふだんのアクセスを一通り試したか |
私は10年ほどエンジニアとして開発に関わってきましたが、Webサーバーを上げたとたん、取引先の古いシステムからの連携だけが失敗するようになったことがあります。原因は、相手が送っていた、決まりから少し外れたヘッダーでした。
サーバーが厳しくなるのは、安全のためには良いことです。ただ、自分の手の届かないクライアントがいる場合は、上げる前に相手と確かめておくと安心です。
Djangoを本番で動かす場合の注意点は、こちらの記事も参考になります。【関連記事】PythonのDjangoとは?
まとめ¶
2026年11月10日、Gunicorn 27.0.0が公開されました。Transfer-Encoding: identityを付けたリクエストや、パスに生の日本語や制御文字を含むリクエストを、400で断るなど、HTTPの決まりに沿わないリクエストを厳しく扱うようになっています。
ブラウザからの普通のアクセスには、ほとんど影響はありません。自作のクライアントや古い機器からの連携がある場合は、上げる前に、送っているリクエストを確かめておきましょう。
ここまでお読みいただきありがとうございました。