AndroidのHttpURLConnection。
これはAndroidじゃなくてJDKのインターフェースの設計の問題なのですが、HttpURLConnection
は入出力エラーが発生した際にIOException
を投げるという規定があります。
問題は、HTTPステータスコードが400番台ないし500番台のコードのボディを読もうとした際に、getInputStream()
を使うと入出力エラー扱いされてIOException
が発生することです。
最近のRESTfulなサーバーインターフェースの設計だと、HTTPステータスコードに意味を持たつつ、レスポンスボディにコンテンツを渡すのが主流ですが、HttpURLConnection
のちょっとしたサンプルでは400 Bad Request
や401 Unauthrized
が返されることを考慮していないことが多いです。
HttpURLConnection
を本格的に使おうとすると、大抵ここで躓くことになります。
正しい実装方法は…?
サーバーがHTTPステータスコード上でエラーを返したときは、getErrorStream()
でしかレスポンスボディを読めません。
定番なのは getResponseCode()
を読んで、getInputStream()
を読むか、getErrorStream()
を判定する実装です。
しかしHttpURLConnection
はどのHTTPステータスコードが入出力エラーに該当するのかを規定していないので、「実装上そうなっている」という経験則で400番台と500番台を特別扱いすることになり、これが非常に気持ち悪いです。
Androidでは FileNotFoundException
をキャッチした上で、getErrorStream()
を読むという実装方法が紹介されていることがあります。
FileNotFoundException
をスローすることはAndroidのコードを読めば分かるのですが、404 NotFound
以外でもFileNotFoundException
をキャッチして処理するのは直感に反していますし、ドキュメントに記載されている内容でもないので、将来的に変更されないとも限りません。あまり推奨された方法ではないと思います。
なおFileNotFoundException
はIOException
のサブクラスなので、IOException
より先にキャッチする必要があります。
401 Unauthrizedの辛さ
アプリとサーバー間でやり取りする場合、認証処理が入るケースが多く、セッションのタイムアウトや、パスワード変更などでサーバーが401エラーを返し、それにハンドリングしなければならないケースはよくあります。
自前で実装する場合にはいろいろ闇があるので注意してください。
まず、前述した getResponseCode()
で識別する方法ですが、古いAndroidでは使えません。
FROYO以前のAndroidには、HTTPステータスコードで401が返却された時点でコネクションをcloseするため、getResponseCode()
の返り値が-1になる致命的なバグがあるためです。
古いAndroid端末もターゲットにする場合は、絶対にHttpURLConnection
を使わない でください。他にもコネクションプール汚染の深刻なバグもあります。Volleyの実装を見てもGoogle自身ですら、FROYO以前のAndroidではApache HTTP Clientで動作するようにしています。
あと、ヘッダにWWW-Authenticate
を付与してくれない場合にIOException
になるケースが非常に厄介です。これは最近のAndroidでも起きるような…。
これについて言えば、仕様上は401 Unauthrized
を返すときは、WWW-Authenticate
の付与が MUST とされているので正しい挙動なのです。サーバーとクライアント間で認証方法が自明であっても付与しましょう。しかしRFC 2616を理解せず、「流行ってるからRESTにしようぜ」的なノリでサーバーを実装する人は後を絶ちません。
稀に、不正な認証リクエストに対してWWW-Authenticate
ヘッダなしで401を返すサーバーも存在します。有名どころではTwitterくらいしか聞きませんけども。