[Android] Volleyライブラリでハマったこと

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

volleyでハマったこと

  • HTTPステータスコード 401 が返却されているはずなのに、ステータスコードが取得できない! (正確には -1が返却されてる)
  • その時は以下の警告が表示された。

    java.io.IOException:No authentication challenges found
    

    原因を探る

    どこでExceptionが出ているのか原因を探ってみました。


  • まずはVolleyのRequestQueue内を探ってみました。
  • Volley#newRequestQueue()

    	public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
            File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
    
            String userAgent = "volley/0";
            try {
                String packageName = context.getPackageName();
                PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
                userAgent = packageName + "/" + info.versionCode;
            } catch (NameNotFoundException e) {
            }
    
            if (stack == null) {
                if (Build.VERSION.SDK_INT >= 9) {
                    stack = new HurlStack();
                } else {
                    // Prior to Gingerbread, HttpUrlConnection was unreliable.
                    // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                    stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
                }
            }
    
            Network network = new BasicNetwork(stack);
    
            RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
            queue.start();
    
            return queue;
        }
    

    newRequestQueueの内部では、Androidのバージョンによって使用するNetwork通信ライブラリを変更しているようです。

    検証していた機種は Android 4.0系だったので HurlStack()を使用していました。

    HurlStack()内部を探ってみました。

    HurlStack#performRequest()

    @Override
        public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
                throws IOException, AuthFailureError {
            String url = request.getUrl();
            HashMap<String, String> map = new HashMap<String, String>();
            map.putAll(request.getHeaders());
            map.putAll(additionalHeaders);
            if (mUrlRewriter != null) {
                String rewritten = mUrlRewriter.rewriteUrl(url);
                if (rewritten == null) {
                    throw new IOException("URL blocked by rewriter: " + url);
                }
                url = rewritten;
            }
            URL parsedUrl = new URL(url);
            HttpURLConnection connection = openConnection(parsedUrl, request);
            for (String headerName : map.keySet()) {
                connection.addRequestProperty(headerName, map.get(headerName));
            }
            setConnectionParametersForRequest(connection, request);
            // Initialize HttpResponse with data from the HttpURLConnection.
            ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
            int responseCode = connection.getResponseCode();
            if (responseCode == -1) {
                // -1 is returned by getResponseCode() if the response code could not be retrieved.
                // Signal to the caller that something was wrong with the connection.
                throw new IOException("Could not retrieve response code from HttpUrlConnection.");
            }
            StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                    connection.getResponseCode(), connection.getResponseMessage());
            BasicHttpResponse response = new BasicHttpResponse(responseStatus);
            response.setEntity(entityFromConnection(connection));
            for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
                if (header.getKey() != null) {
                    Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                    response.addHeader(h);
                }
            }
            return response;
        }
    

    デバッグしていくと、上記23行目のconnection.getResponseCode()からExceptionがthrowされてました。(27行目のExceptionではない。)

    HttpURLConnectionでExceptionが出てたので、よくよく仕様を確認してみると…

    HttpURLConnection.html#getResponseCode()


    前述の応答からは整数 200 と 401 をそれぞれ取り出します。応答が識別できない (有効な HTTP でない) 場合は -1 を返します。

    どうやらアプリ側は悪くない模様?
    有効なHTTPでない って意味不。

    調べた結果、RFC規格で401のステータスコードは WWW-Authenticateのヘッダーを返さないといけないらしいです。
    ということで、サーバAPIの返却値のヘッダーに WWW-Authenticateを入れてもらったら無事に通信できました!

    この辺のstackoverfrowにも書いてますね…

    stackoverflow

    .oO(今回はサーバ側変更できたからいいけど、変更不可だったら無理矢理HTTPClient使うとかしないと詰む?)

    アプリ開発者もサーバ側の知識が必要だな〜と感じました。というお話でした。