HTTPに固有の問題を解決するには、HttpURLConnectionを使う必要があります。
どうしても、HttpURLConnectionを使わなければならないケースについて、簡単に見てみます。
前章でも書きましたが、URLConnectionを使った場合、コネクションタイムアウトとリードタイムアウトが切り分けられないケースがあります。
HttpURLConnectionには、connectメソッドが存在し、コネクションタイムアウトはこのメソッドで発生するので、切り分けることが出来ます。
認証が必要なケースや、リソースをキャッシュする様なケースでは、HTTPステータスコードの判定は不可避です。
URLConnectionは、サーバがリダイレクト指示をレスポンスした場合、内部で自動的にリダイレクトを行い、その結果を返します。
リクエスト元が、サーバやアプリであることを前提として構築されたサーバであれば、問題ないかもしれませんが、通常はユーザがWebブラウザで参照するサーバに対して、プログラムからアクセスして、結果を機械的に解析するようなシステムでは、リダイレクトは厄介です。
また、障害対応という意味でも、サーバから指定されたリダイレクト先がダウンしていた様な場合、自動的にリダイレクトされてしまうと、リクエスト先すら不明のまま障害となってしまうので、障害原因の切り分けが絶望的になります。
HttpURLConnectionのsetInstanceFollowRedirectsメソッドを使えば、HttpURLConnectionのインスタンスごとに、リダイレクトの抑止を行うことが出来ます。
サーバ上で稼働するプログラムでの、getFollowRedirectsの使用は極力避けてください。同じクラスローダーでロードされたHttpURLConnection全てに影響してしまいます。
あと、自動的なリダイレクトで注意が必要なのが、URLのクエリ文字列です。
サーバが、リダイレクト先のLocationヘッダに、リクエストのクエリ文字列を設定でもしてくれない限り(ありそうもないことです)、リダイレクト時にクエリ文字列は設定されません。
但し、POSTでリダイレクトする場合は、送信したPOSTデータはリダイレクト先にも再送信されます。
問題となりそうなステータスコードには、以下のようなものがあります。
リクエストされたURLのリソースが、恒久的に移動されたことを伝えるレスポンスコードです。
仕様上は、リダイレクト先のリソースは、本来リクエストされたURLのリソースと同じものであることを想定しています。
URLConnectionは、POSTによるリクエストに対してこのコードが返されると、GETでリダイレクトします。
リクエストされたURLのリソースが、一時的に移動されたことを伝えるレスポンスコードです。
仕様上は、リダイレクト先のリソースは、本来リクエストされたURLのリソースと同じものであることを想定しています。
但し、仕様とは異なる使い方が横行したので、303や307が追加されたという経緯があります。
URLConnectionは、POSTによるリクエストに対してこのコードが返されると、GETでリダイレクトします。
サーバが意図的に、リクエストされたURLとは異なるリソースを参照させたいことを伝えるレスポンスコードです。
仕様上は、リダイレクト先のリソースは、本来リクエストされたURLのリソースと異なるものであることを想定しています。
URLConnectionは、POSTによるリクエストに対してこのコードが返されると、GETでリダイレクトします。
リクエストされたURLのリソースが、一時的に移動されたことを伝えるレスポンスコードです。
仕様上は、リダイレクト先のリソースは、本来リクエストされたURLのリソースと同じものであることを想定しています。
URLConnectionは、POSTによるリクエストに対してこのコードが返されると、POSTでリダイレクトします。
リクエストされたURLのリソースが、恒久的に移動されたことを伝えるレスポンスコードです。
仕様上は、リダイレクト先のリソースは、本来リクエストされたURLのリソースと同じものであることを想定しています。
今回のテスト環境におけるURLConnectionは、リクエストに対してこのコードが返されても、自動的にリダイレクトしません。
308は、最近(2015年)になって追加されたレスポンスコードなので、2014年リリースのJava8では未サポートです。
※Java11でも自動的にはリダイレクトされません。そもそも、HttpURLConnectionのレスポンスコードを表すフィールドにも、308は追加されていません。Java11からはjava.net.http(正確にはjava9から実験的にjdk.incubator.httpclientという名前で)というモジュールが追加されて、HTTP関連のクラスが実装されたので、そちらを使えということなのでしょう(java.net.httpモジュールには、WebSocketクライアントも実装されているので、JavaSEの標準機能だけで、Web系のクライアントを構築できるようになりました)。
サーバ側の運用にもよりますが、302と303は、本来アクセスすべきリソースとは異なるリソースを返している可能性があります。「只今メンテナンス中です」というページにリダイレクトしているのかもしれません。
しかし、307は、主系サーバがメンテナンス中なので副系サーバにリダイレクトしているのかもしれません。
このようなケースでは、302と303はエラーとして、307は正常としなければいけません。
但し、リダイレクト先が再びリダイレクトを返してきた場合、そのまま従うと「永久ループ!」となりかねないので、注意が必要です。
URLに該当するリソースが存在しないケースや、サーバの内部エラーが発生したケースのように、エラー系のHTTPステータスコードが返される場合、サーバが障害の原因判明に役に立ちそうな情報を、コンテンツの内容として出力している可能性があります。
よく、WebブラウザでURLを間違えたときなどに、「Not Found」などと表示されるあれです。
しかし、URLConnectionではこの内容にアクセスする方法がありません。
HttpURLConnectionのgetErrorStreamメソッドを使えば、そのような情報を取得することができます。「なんで別にしたんだ!?!!」とよく昔思いました。まあ、レスポンスコード400以上を例外とした時点で、しょうがなかったんでしょう。
前章で提起したいくつかの問題を解決する内容となっています。
1 | public static ResponseData post(String urlStr, |
2 | String postData, |
3 | int connectTimeout, |
4 | int readTimeout) throws Exception { |
5 | URL url = new URL(urlStr); |
6 | HttpURLConnection connection = (HttpURLConnection)url.openConnection(); |
7 | connection.setConnectTimeout(connectTimeout); |
8 | connection.setReadTimeout(readTimeout); |
9 | connection.setInstanceFollowRedirects(false); |
10 | connection.setRequestProperty("Connection", "close"); |
11 | connection.setDoOutput(true); |
12 | try { |
13 | connection.connect(); |
14 | } catch (SocketTimeoutException e) { |
15 | throw new ConnectTimeoutException(e); |
16 | } |
17 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
18 | String contentType = null; |
19 | String location = null; |
20 | int httpStatusCode = -1; |
21 | try { |
22 | OutputStream outputStream = null; |
23 | InputStream inputStream = null; |
24 | try { |
25 | outputStream = connection.getOutputStream(); |
26 | outputStream.write(postData.getBytes("ISO8859_1")); |
27 | } finally { |
28 | safetyClose(outputStream); |
29 | } |
30 | try { |
31 | inputStream = connection.getInputStream(); |
32 | int availableSize = inputStream.available(); |
33 | byte[] bytes = new byte[availableSize + 1]; |
34 | while (true) { |
35 | int size = inputStream.read(bytes); |
36 | if (-1 == size) { |
37 | break; |
38 | } |
39 | byteArrayOutputStream.write(bytes, 0, size); |
40 | } |
41 | contentType = connection.getHeaderField("Content-Type"); |
42 | location = connection.getHeaderField("Location"); |
43 | httpStatusCode = connection.getResponseCode(); |
44 | } finally { |
45 | safetyClose(inputStream); |
46 | } |
47 | } catch (IOException e) { |
48 | try { |
49 | httpStatusCode = connection.getResponseCode(); |
50 | } catch (Exception e2) { |
51 | httpStatusCode = -1; |
52 | } |
53 | if (-1 != httpStatusCode) { |
54 | InputStream errorStream = connection.getErrorStream(); |
55 | if (null != errorStream) { |
56 | try { |
57 | int availableSize = errorStream.available(); |
58 | byte[] bytes = new byte[availableSize + 1]; |
59 | while (true) { |
60 | int size = errorStream.read(bytes); |
61 | if (-1 == size) { |
62 | break; |
63 | } |
64 | byteArrayOutputStream.write(bytes, 0, size); |
65 | } |
66 | contentType = connection.getHeaderField("Content-Type"); |
67 | } catch (Exception ex) { |
68 | log.debug("", e); |
69 | } finally { |
70 | safetyClose(errorStream); |
71 | } |
72 | } |
73 | } else { |
74 | throw e; |
75 | } |
76 | } finally { |
77 | connection.disconnect(); |
78 | } |
79 | byte[] bytes = byteArrayOutputStream.toByteArray(); |
80 | return new ResponseData(httpStatusCode, contentType, location, bytes); |
81 | } |
82 | |
83 | public static class ResponseData { |
84 | private int httpStatusCode = -1; |
85 | private String contentType = null; |
86 | private String location = null; |
87 | private byte[] bytes = null; |
88 | public ResponseData(int httpStatusCode, String contentType, String location, byte[] bytes) { |
89 | this.httpStatusCode = httpStatusCode; |
90 | this.contentType = contentType; |
91 | this.location = location; |
92 | this.bytes = bytes; |
93 | } |
94 | public byte[] getBytes() { return Arrays.copyOf(bytes, bytes.length); } |
95 | public int getHttpStatusCode() { return httpStatusCode; } |
96 | public String getContentType() { return contentType; } |
97 | public String getLocation() { return location; } |
98 | } |
9行目で、このインスタンスによるリダイレクトの自動実行を抑止しています。
13行目でサーバとの接続を行います。ここでSocketTimeoutExceptionがスローされた場合、コネクトタイムアウトを表す独自の例外を投げます。これで、コネクトタイムアウトとリードタイムアウトの切り分けができます。
メソッドの呼び出し元で、この例外をキャッチして、必要に応じてリトライを行います。
22行目から46行目までは、変数の宣言位置は変わっていますが、処理の内容自体はURLConnectionを使用したPOSTのケースとほぼ同じです。
違いは、42行目でヘッダからLocationの値を取得している箇所と、43行目でHTTPステータスを取得している箇所です。Locationの値は、リダイレクト先のURLです。
メソッドが返すクラスには、ステータスコードとLocationヘッダの値が追加されているので、呼び出し元でステータスコードを判定し、エラーとするかリダイレクトするかを判定できます。
47行目で、サーバとの接続処理以外で発生した全てのIOExceptionをキャッチします。
HttpURLConnectionは、サーバが400以上のステータスコードを返した時にIOExceptionをスローするので、このキャッチ句で、正常時と同じ形の戻り値を編集して呼び元に返します。
49行目でステータスコードを取得しています。ステータスコードはサーバからの返却値なので、サーバからデータを取得できていないケースでは、この時点でサーバデータの取得を行う為例外が発生する可能性があります。例外をキャッチした場合は、ステータスコードを-1とします。
ステータスコードがサーバから返却されていない場合は、74行目で、キャッチした例外をそのままスローします。リードタイムアウトなどが、このケースに当たります。
ステータスコードがサーバから返却された場合は、サーバからのレスポンスデータの取得を試みます。
これで、このpostメソッドを呼び出した結果、ConnectTimeoutExceptionをキャッチすればコレクトタイムアウト、それ以外の例外をキャッチすればエラー、正常に戻り値が取得できれば、サーバから何らかの結果を取得できたと判断できます。
※もちろん、このコードが完璧に動作することを保証するものではありませんので、注意してくださいね。
URLConnectionを使用したPOSTのケースとの違いは、connection.connect()の中で、サーバとの接続を行っている箇所だけです。
1.今更ながらの、HttpURLConnection Get編
2.今更ながらの、HttpURLConnection Post編
3.今更ながらの、HttpURLConnection 障害処理編
4.今更ながらの、HttpURLConnection (やっと)HttpURLConnectionを使う編