読者です 読者をやめる 読者になる 読者になる

なるようになるかも

力は多くの場合、その人の思いを超えない。

AndroidのHttpURLConnection。

これはAndroidじゃなくてJDKのインターフェースの設計の問題なのですが、HttpURLConnectionは入出力エラーが発生した際にIOExceptionを投げるという規定があります。

問題は、HTTPステータスコードが400番台ないし500番台のコードのボディを読もうとした際に、getInputStream()を使うと入出力エラー扱いされてIOExceptionが発生することです。

最近のRESTfulなサーバーインターフェースの設計だと、HTTPステータスコードに意味を持たつつ、レスポンスボディにコンテンツを渡すのが主流ですが、HttpURLConnectionのちょっとしたサンプルでは400 Bad Request401 Unauthrizedが返されることを考慮していないことが多いです。

HttpURLConnectionを本格的に使おうとすると、大抵ここで躓くことになります。

正しい実装方法は…?

サーバーがHTTPステータスコード上でエラーを返したときは、getErrorStream()でしかレスポンスボディを読めません。

定番なのは getResponseCode()を読んで、getInputStream()を読むか、getErrorStream()を判定する実装です。

しかしHttpURLConnectionはどのHTTPステータスコードが入出力エラーに該当するのかを規定していないので、「実装上そうなっている」という経験則で400番台と500番台を特別扱いすることになり、これが非常に気持ち悪いです。

Androidでは FileNotFoundExceptionをキャッチした上で、getErrorStream()を読むという実装方法が紹介されていることがあります。

FileNotFoundExceptionをスローすることはAndroidのコードを読めば分かるのですが、404 NotFound以外でもFileNotFoundExceptionをキャッチして処理するのは直感に反していますし、ドキュメントに記載されている内容でもないので、将来的に変更されないとも限りません。あまり推奨された方法ではないと思います。

なおFileNotFoundExceptionIOExceptionのサブクラスなので、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くらいしか聞きませんけども。