JavaでHTTPクライアントを作ろうと思ったときには、おそらく二つくらいのアプローチがあって、ひとつがApache JakartaプロジェクトのHTTPClientを使う方法、もうひとつがjava.net.HttpURLConnection を使う方法だ。まあもちろん、自前でTCPクライアントの上に乗せてもいいのだが、そんな車輪の再発明するくらいならもっと別のことに労力を使うべきだと思う。
というわけで、そのうちひとつのjava.net.HttpURLConnectionの件。HTTPは最初だけ大文字、後小文字の癖に、URLは全部大文字というよくわからんネーミングセンスについてはとりあえず置いておくとして、設計的によくわからないというか、何がしたいのか意味が不明なところがあるのでそれについて書く。
http://java.sun.com/j2se/1.5.0/docs/api/java/net/HttpURLConnection.html
HttpURLConnectionは、URLオブジェクトからコネクションを張ることで以下のように接続を行う。
URL url = new URL("http://example.com/"); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setRequestMethod("GET"); conn.connect();
で、その後データを取得する。この際使用するのがBufferedReaderなのはバイナリが帰ってくるかもしれないから仕方ない。
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
問題なのは、この
conn.getInputStream()
が、throw IOExceptionとなっていることなのだ。これはリファレンスによれば、
と書いてある。これ、実際にどういうときに起きるかといえば、他にもあるかもしれないがサーバがエラーを返したときなのだ。つまり、サーバが4xxまたは5xxのエラーレスポンスを返したときには、なぜかInputStreamは生成されない。nullが返るのではなくてExceptionが飛んでくる。そしてその場合にはgetErrorStream()で取得したStreamにHTTP ResponseのBodyが返ってくる。
あるサンプルソースコードはこのため、こんなことをやっている(http://java.sun.com/j2se/1.5.0/docs/guide/net/http-keepalive.html)。
try { URL a = new URL(args[0]); URLConnection urlc = a.openConnection(); is = conn.getInputStream(); int ret = 0; while ((ret = is.read(buf)) > 0) { processBuf(buf); } // close the inputstream is.close(); } catch (IOException e) { try { respCode = ((HttpURLConnection)conn).getResponseCode(); es = ((HttpURLConnection)conn).getErrorStream(); int ret = 0; // read the response body while ((ret = es.read(buf)) > 0) { processBuf(buf); } // close the errorstream es.close(); } catch(IOException ex) { // deal with the exception } }
このソースコードは、まあ目的が違って永続セッションを張ろうとしているので(サーバがエラーを返した時点で終了するのがクライアントとして正しい動作なので)これでいいのだろうけども、「とりあえずInputStreamを取得して、Exceptionが出たらErrorStream使おう」とかいう態度はどうかと思う。例外は、正常形で投げられてはいけないのだ。
HTTPClientを書こうとした人間が、サーバの4xxエラーまたは5xxエラーを正常系とみなすか異常系とみなすかは不明である。サーバにとって異常系なのは明らかだが、例えばサーバのエラーやリンク切れを発見するテストクライアントであれば、異常系ではない。
4xxや5xxがクライアントにとっても異常系だと主張するにしても、getInputStream()で例外を投げるのはタイミングがおかしい。エラーはbodyを取得する前にわかっているのだから、その前に例外を投げるべきだ。
あるいは、以下のような実装も考えられる。
conn.connect(); InputStream stream; if(conn.isSuccess()){ stream = conn.getInputStream(); }else{ stream = conn.getErrorStream(); }
しかし、isSuccessのようなメソッドは実装されていない。また、本質的な話としてこのHttpURLConnection#getInputStreamが例外を投げるケースはResponse Codeが何番なのかということはドキュメントのどこにも書いていないことが挙げられる。したがって、このようなisSuccessを自前で作ることすらできないのだ。
実際に正しくやろうと思ったら現状では以下のようにするしかないが、これで正しいかどうかは誰も保障してくれないのだ。
InputStream stream; int responseCode = conn.getResponseCode(); if(responseCode / 100 == 4 || responseCode / 100 == 5){ stream = conn.getErrorStream(); }else{ stream = conn.getInputStream(); }