Magnolia Tech

いつもコードのことばかり考えている人のために。

HTTP responseのstatus-line、reason-phraseの内容にどこまでこだわるか?

scalatraのメンテナンスでservletのdeprecatedなメソッドを使っている箇所を直そうと思ったら単純には行かず、延々とHTTP protocolのstatus-lineに入るreason-phraseについて調べることになりました。

せっかくなので、その結果をまとめておきます。

status-lineのreason-phraseとは?

まずはstatus-lineのreason-pheraseについておさらいします。

おなじみHTTP protocolのresponseの1行目はstatus-lineと呼ばれ、「HTTP/1.1 200 OK」や、「HTTP/1.1 404 Not Found」といった内容がサーバサイトの処理結果に応じてクライアントに返されます。

このstatus-lineの内容はRFC 7230で以下のように定義されています。

status-line = HTTP-version SP status-code SP reason-phrase CRLF

status-codeは3桁のコードで、HTTP requestの成否が返されます。その一覧はRFC7231で確認することができます。

続くreason-phraseには人間が理解できるようにstatus-codeの内容を簡潔に示すテキストが入ります。RFC 7230では以下のように定義されていて、あまり現代では必要性無さそうです。

The reason-phrase element exists for the sole purpose of providing a textual description associated with the numeric status code, mostly out of deference to earlier Internet application protocols that were more frequently used with interactive text clients.

更には続けてクライアントソフトは無視すべきであると書かれています。

A client SHOULD ignore the reason-phrase content.

reason-phraseの形式は先ほどのRFC 7230で以下のように定義されています。

reason-phrase  = *( HTAB / SP / VCHAR / obs-text )

先頭がアスタリスクなので、全て省略しても規格上は正しい、つまりreason-phraseは規格上省略可です。

一応RFC7231にstatus-codeごとにreason-phraseの推奨案が記載されていますが、あくまで推奨なので独自に変えたとしてもプロトコルの動作に影響を与えないこととなっています(それで挙動が変わることが有れば実装の方が誤り、という事です)。

サーバアプリケーションでのreason-phraseの設定処理

普段サーバアプリケーションを書いていてreason-phraseのことをいちいち気にすることは無いと思いますが、これは一般的にアプリケーションサーバ側でよしなに処理してくれるからです。

いくつか実装を見てみましょう。

Sinatra + WEBrick(Ruby - Rack)

Sinatra + WEBrickはご存じの通り、Rackというインタフェースを通じて接続されています。Rackのインタフェース上、status-codeしか渡せないので、Sinatraではreason-phraseを設定していませんが、WEBrick側でreason-phraseを設定してくれます。

https://github.com/ruby/webrick/blob/43fc65e0619f033d02e8f4a0d647687f20939240/lib/webrick/httpresponse.rb#L125-L128

ちなみにRackの中でもreason-phraseが定義されていますが、WEBrickでは独自に定義されています。

https://github.com/rack/rack/blob/6b942ff543416e0c82196f0790d4915c7eead4cb/lib/rack/utils.rb#L487-L552

Jetty(Java - Servlet)

JavaServlet、例えばJetty実装では、以下のように定義されています。

https://github.com/eclipse/jetty.project/blob/6fd3351272ba949dc80d160c18f0e403757a0c0d/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java#L27-L33

Servletにはエラーレスポンスを即時に返すsendErrorというメソッドが有りますが、以下のようにreason-phraseを取得しています。

https://github.com/eclipse/jetty.project/blob/829fa4fe9bc650e336e7412aae6e6bffa9b37778/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L635-L641

なお、Servletのもう一つの実装、Apache Tomcatではver 8.5以降でreason-phraseはデフォルトでは送信されなくなっていて、オプションで送信するように変更可能なものの、その設定自体は非推奨となっています。また、Tomcat 9からはこのオプションすら削除されるそうです。

60362 – Missing reason phrase in response

独自のreason-phraseを設定する

冒頭で説明した通り、reason-phraseは規格上は推奨案は有っても厳格な定義はされていません。例えば先ほどのServletSendErrorメソッドの第二引数に任意の文字列を渡せば独自のreason-phraseを設定することができ、規格上も問題ありません。

RackやPSGI(Perl)と言った言語でのインタフェースではstatus-codeしか渡さないので、アプリ側から独自のreason-phraseを定義する方法は無さそうです(serverの実装しだい…でも見たことが無い…)。

HTTP/2

最近普及してきたHTTP/2では規格自体にreason-phraseが無いそうです。

github.com

Scalatraのメンテで遭遇した事象

古いServletの規格では2つの引数を取るsetStatusというメソッドが用意されていて、status-codeと独自のreason-phraseを設定し、かつbody部を定義することができました。現在最新のScalatra 2.5.1ではこれが使われています(reason-phraseが設定されていない時は、status-codeのみをセットするsetStatusが使われるが、ユーザーコード側からは見えないように隠蔽されている)。

現在(といってもServlet 2.1以降)では2つの引数を取るsetStatusは非推奨になり、独自のreason-phraseを設定したい場合は、body部無しにただしにエラーを返す(エラー例外を送出し、エラーログに出力される)SendErrorメソッドを使うしか有りません。

現在のServletではエラーが発生した時に、body無しで簡潔にエラー内容を伝えたい時(ログに残したい時)に使う思想のようです。しかし先ほどのTomcatのように、そもそもreason-phraseを一切送信しない実装も有るので、必ずクライアント側に送信される保証は無く、あまり使い道は無さそうです。

この解決はGitHubのissueで!

結論

というわけで、ここまで見てきて…

  • 設定内容がそもそも規格上保証されていない(RFC 7231)
  • アプリ側で自由に設定できない規格/実装がある(Rackは設定できない、Servletはエラー時のみ設定可)
  • 送信されない実装が有る(apache Tomcatなど)
  • HTTP/2では規格上存在しない
  • そもそも規格でクライアントは無視すべきとまで書かれている(RFC 7230)

現代では「status-lineのreason-phraseにこだわってはいけないこと」がよく分かりました。HTTP responseの死活チェックなどで「200 OK」をチェックしないように気をつけましょう!