예제 #1
0
        /// <summary>
        /// Creates, configures and processes an asynchronous request to the indicated resource.
        /// </summary>
        /// <returns>Task in which the request is executed</returns>
        /// <param name="request">Request provided by <see cref="System.Net.Http.HttpClient"/></param>
        /// <param name="cancellationToken">Cancellation token.</param>
        protected override async Task <HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            AssertSelf();
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            if (!request.RequestUri.IsAbsoluteUri)
            {
                throw new ArgumentException("Must represent an absolute URI", "request");
            }

            var redirectState = new RequestRedirectionState
            {
                NewUrl          = request.RequestUri,
                RedirectCounter = 0,
                Method          = request.Method
            };

            while (true)
            {
                URL               java_url        = new URL(EncodeUrl(redirectState.NewUrl));
                URLConnection     java_connection = java_url.OpenConnection();
                HttpURLConnection httpConnection  = await SetupRequestInternal(request, java_connection);

                HttpResponseMessage response = await ProcessRequest(request, java_url, httpConnection, cancellationToken, redirectState);

                if (response != null)
                {
                    return(response);
                }

                if (redirectState.NewUrl == null)
                {
                    throw new InvalidOperationException("Request redirected but no new URI specified");
                }
                request.Method = redirectState.Method;
            }
        }
예제 #2
0
        bool HandleRedirect(HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet)
        {
            if (!AllowAutoRedirect)
            {
                disposeRet = false;
                return(true);                // We shouldn't follow and there's no data to fetch, just return
            }
            disposeRet = true;

            redirectState.NewUrl = null;
            switch (redirectCode)
            {
            case HttpStatusCode.MultipleChoices:                       // 300
                break;

            case HttpStatusCode.Moved:                                 // 301
            case HttpStatusCode.Redirect:                              // 302
            case HttpStatusCode.SeeOther:                              // 303
                redirectState.Method = HttpMethod.Get;
                break;

            case HttpStatusCode.TemporaryRedirect:                     // 307
                break;

            default:
                if ((int)redirectCode >= 300 && (int)redirectCode < 400)
                {
                    throw new InvalidOperationException($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported");
                }
                return(false);
            }

            IDictionary <string, IList <string> > headers = httpConnection.HeaderFields;
            IList <string> locationHeader;

            if (!headers.TryGetValue("Location", out locationHeader) || locationHeader == null || locationHeader.Count == 0)
            {
                throw new InvalidOperationException($"HTTP connection redirected with code {redirectCode} ({(int)redirectCode}) but no Location header found in response");
            }

            if (locationHeader.Count > 1 && Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"More than one location header for HTTP {redirectCode} redirect. Will use the first one.");
            }

            redirectState.RedirectCounter++;
            if (redirectState.RedirectCounter >= MaxAutomaticRedirections)
            {
                throw new WebException($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)");
            }

            string redirectUrl = locationHeader [0];
            string protocol    = httpConnection.URL?.Protocol;

            if (redirectUrl.StartsWith("//", StringComparison.Ordinal))
            {
                // When redirecting to an URL without protocol, we use the protocol of previous request
                // See https://tools.ietf.org/html/rfc3986#section-5 (example in section 5.4)
                redirectUrl = protocol + ":" + redirectUrl;
            }
            redirectState.NewUrl = new Uri(redirectUrl, UriKind.Absolute);
            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Debug, LOG_APP, $"Request redirected to {redirectState.NewUrl}");
            }

            return(true);
        }
예제 #3
0
        async Task <HttpResponseMessage> DoProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
        {
            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"{this}.DoProcessRequest ()");
            }

            if (cancellationToken.IsCancellationRequested)
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, " cancelled");
                }

                cancellationToken.ThrowIfCancellationRequested();
            }

            try {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"  connecting");
                }

                await ConnectAsync(httpConnection, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);

                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"  connected");
                }
            } catch (Java.Net.ConnectException ex) {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Connection exception {ex}");
                }
                // Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler
                throw new WebException(ex.Message, ex, WebExceptionStatus.ConnectFailure, null);
            }

            if (cancellationToken.IsCancellationRequested)
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, " cancelled");
                }

                await DisconnectAsync(httpConnection).ConfigureAwait(continueOnCapturedContext: false);

                cancellationToken.ThrowIfCancellationRequested();
            }

            CancellationTokenRegistration cancelRegistration = default(CancellationTokenRegistration);
            HttpStatusCode statusCode    = HttpStatusCode.OK;
            Uri            connectionUri = null;

            try {
                cancelRegistration = cancellationToken.Register(() => {
                    DisconnectAsync(httpConnection).ContinueWith(t => {
                        if (t.Exception != null)
                        {
                            Logger.Log(LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}");
                        }
                    }, TaskScheduler.Default);
                }, useSynchronizationContext: false);

                if (httpConnection.DoOutput)
                {
                    using (var stream = await request.Content.ReadAsStreamAsync().ConfigureAwait(false)) {
                        await stream.CopyToAsync(httpConnection.OutputStream, 4096, cancellationToken)
                        .ConfigureAwait(false);
                    }
                }

                statusCode = await Task.Run(() => (HttpStatusCode)httpConnection.ResponseCode).ConfigureAwait(false);

                connectionUri = new Uri(httpConnection.URL.ToString());
            } finally {
                cancelRegistration.Dispose();
            }

            if (cancellationToken.IsCancellationRequested)
            {
                await DisconnectAsync(httpConnection).ConfigureAwait(continueOnCapturedContext: false);

                cancellationToken.ThrowIfCancellationRequested();
            }

            // If the request was redirected we need to put the new URL in the request
            request.RequestUri = connectionUri;
            var ret = new AndroidHttpResponseMessage(javaUrl, httpConnection)
            {
                RequestMessage = request,
                ReasonPhrase   = httpConnection.ResponseMessage,
                StatusCode     = statusCode,
            };

            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"Status code: {statusCode}");
            }

            bool disposeRet;

            if (HandleRedirect(statusCode, httpConnection, redirectState, out disposeRet))
            {
                if (disposeRet)
                {
                    ret.Dispose();
                    ret = null;
                }
                return(ret);
            }

            switch (statusCode)
            {
            case HttpStatusCode.Unauthorized:
            case HttpStatusCode.ProxyAuthenticationRequired:
                // We don't resend the request since that would require new set of credentials if the
                // ones provided in Credentials are invalid (or null) and that, in turn, may require asking the
                // user which is not something that should be taken care of by us and in this
                // context. The application should be responsible for this.
                // HttpClientHandler throws an exception in this instance, but I think it's not a good
                // idea. We'll return the response message with all the information required by the
                // application to fill in the blanks and provide the requested credentials instead.
                //
                // We return the body of the response too, but the Java client will throw
                // a FileNotFound exception if we attempt to access the input stream.
                // Instead we try to read the error stream and return an default message if the error stream isn't readable.
                ret.Content = GetErrorContent(httpConnection, new StringContent("Unauthorized", Encoding.ASCII));
                CopyHeaders(httpConnection, ret);

                if (ret.Headers.WwwAuthenticate != null)
                {
                    ProxyAuthenticationRequested = false;
                    CollectAuthInfo(ret.Headers.WwwAuthenticate);
                }
                else if (ret.Headers.ProxyAuthenticate != null)
                {
                    ProxyAuthenticationRequested = true;
                    CollectAuthInfo(ret.Headers.ProxyAuthenticate);
                }

                ret.RequestedAuthentication = RequestedAuthentication;
                return(ret);
            }

            if (!IsErrorStatusCode(statusCode))
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Reading...");
                }
                ret.Content = GetContent(httpConnection, httpConnection.InputStream);
            }
            else
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Status code is {statusCode}, reading...");
                }
                // For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream.
                // Instead we try to read the error stream and return an empty string if the error stream isn't readable.
                ret.Content = GetErrorContent(httpConnection, new StringContent(String.Empty, Encoding.ASCII));
            }

            CopyHeaders(httpConnection, ret);

            IEnumerable <string> cookieHeaderValue;

            if (!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null)
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"No cookies");
                }
                return(ret);
            }

            try {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Parsing cookies");
                }
                CookieContainer.SetCookies(connectionUri, String.Join(",", cookieHeaderValue));
            } catch (Exception ex) {
                // We don't want to terminate the response because of a bad cookie, hence just reporting
                // the issue. We might consider adding a virtual method to let the user handle the
                // issue, but not sure if it's really needed. Set-Cookie header will be part of the
                // header collection so the user can always examine it if they spot an error.
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Failed to parse cookies in the server response. {ex.GetType ()}: {ex.Message}");
                }
            }

            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"Returning");
            }
            return(ret);
        }
예제 #4
0
        Task <HttpResponseMessage> ProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
        {
            cancellationToken.ThrowIfCancellationRequested();
            httpConnection.InstanceFollowRedirects = false;             // We handle it ourselves
            RequestedAuthentication      = null;
            ProxyAuthenticationRequested = false;

            return(DoProcessRequest(request, javaUrl, httpConnection, cancellationToken, redirectState));
        }
예제 #5
0
        bool HandleRedirect(HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet)
        {
            if (!AllowAutoRedirect)
            {
                disposeRet = false;
                return(true);                // We shouldn't follow and there's no data to fetch, just return
            }
            disposeRet = true;

            redirectState.NewUrl        = null;
            redirectState.MethodChanged = false;
            switch (redirectCode)
            {
            case HttpStatusCode.MultipleChoices:                       // 300
                break;

            case HttpStatusCode.Moved:                                 // 301
            case HttpStatusCode.Redirect:                              // 302
            case HttpStatusCode.SeeOther:                              // 303
                redirectState.MethodChanged = redirectState.Method != HttpMethod.Get;
                redirectState.Method        = HttpMethod.Get;
                break;

            case HttpStatusCode.NotModified:         // 304
                disposeRet = false;
                return(true);                        // Not much happening here, just return and let the client decide

            // what to do with the response

            case HttpStatusCode.TemporaryRedirect:                     // 307
                break;

            default:
                if ((int)redirectCode >= 300 && (int)redirectCode < 400)
                {
                    throw new InvalidOperationException($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported");
                }
                return(false);
            }

            IDictionary <string, IList <string> > headers = httpConnection.HeaderFields;
            IList <string> locationHeader;

            if (!headers.TryGetValue("Location", out locationHeader) || locationHeader == null || locationHeader.Count == 0)
            {
                // As per https://tools.ietf.org/html/rfc7231#section-6.4.1 the reponse isn't required to contain the Location header and the
                // client should act accordingly. Since it is not documented what the action in this case should be, we're following what
                // Xamarin.iOS does and simply return the content of the request as if it wasn't a redirect.
                disposeRet = false;
                return(true);
            }

            if (locationHeader.Count > 1 && Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"More than one location header for HTTP {redirectCode} redirect. Will use the first one.");
            }

            redirectState.RedirectCounter++;
            if (redirectState.RedirectCounter >= MaxAutomaticRedirections)
            {
                throw new WebException($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)");
            }

            string redirectUrl = locationHeader [0];
            string protocol    = httpConnection.URL?.Protocol;

            if (redirectUrl.StartsWith("//", StringComparison.Ordinal))
            {
                // When redirecting to an URL without protocol, we use the protocol of previous request
                // See https://tools.ietf.org/html/rfc3986#section-5 (example in section 5.4)
                redirectUrl = protocol + ":" + redirectUrl;
            }
            redirectState.NewUrl = new Uri(redirectUrl, UriKind.Absolute);
            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Debug, LOG_APP, $"Request redirected to {redirectState.NewUrl}");
            }

            return(true);
        }
예제 #6
0
        async Task <HttpResponseMessage> DoProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
        {
            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"{this}.DoProcessRequest ()");
            }

            if (cancellationToken.IsCancellationRequested)
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, " cancelled");
                }

                cancellationToken.ThrowIfCancellationRequested();
            }

            try {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"  connecting");
                }

                await ConnectAsync(httpConnection, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);

                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"  connected");
                }
            } catch (Java.Net.ConnectException ex) {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Connection exception {ex}");
                }
                // Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler
                throw new WebException(ex.Message, ex, WebExceptionStatus.ConnectFailure, null);
            }

            if (cancellationToken.IsCancellationRequested)
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, " cancelled");
                }

                await DisconnectAsync(httpConnection).ConfigureAwait(continueOnCapturedContext: false);

                cancellationToken.ThrowIfCancellationRequested();
            }

            CancellationTokenRegistration cancelRegistration = default(CancellationTokenRegistration);
            HttpStatusCode statusCode    = HttpStatusCode.OK;
            Uri            connectionUri = null;

            try {
                cancelRegistration = cancellationToken.Register(() => {
                    DisconnectAsync(httpConnection).ContinueWith(t => {
                        if (t.Exception != null)
                        {
                            Logger.Log(LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}");
                        }
                    }, TaskScheduler.Default);
                }, useSynchronizationContext: false);

                if (httpConnection.DoOutput)
                {
                    await WriteRequestContentToOutput(request, httpConnection, cancellationToken);
                }

                statusCode = await Task.Run(() => (HttpStatusCode)httpConnection.ResponseCode).ConfigureAwait(false);

                connectionUri = new Uri(httpConnection.URL.ToString());
            } finally {
                cancelRegistration.Dispose();
            }

            if (cancellationToken.IsCancellationRequested)
            {
                await DisconnectAsync(httpConnection).ConfigureAwait(continueOnCapturedContext: false);

                cancellationToken.ThrowIfCancellationRequested();
            }

            // If the request was redirected we need to put the new URL in the request
            request.RequestUri = connectionUri;
            var ret = new AndroidHttpResponseMessage(javaUrl, httpConnection)
            {
                RequestMessage = request,
                ReasonPhrase   = httpConnection.ResponseMessage,
                StatusCode     = statusCode,
            };

            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"Status code: {statusCode}");
            }

            if (!IsErrorStatusCode(statusCode))
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Reading...");
                }
                ret.Content = GetContent(httpConnection, httpConnection.InputStream);
            }
            else
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Status code is {statusCode}, reading...");
                }
                // For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream.
                // Instead we try to read the error stream and return an empty string if the error stream isn't readable.
                ret.Content = GetErrorContent(httpConnection, new StringContent(String.Empty, Encoding.ASCII));
            }

            bool disposeRet;

            if (HandleRedirect(statusCode, httpConnection, redirectState, out disposeRet))
            {
                if (redirectState.MethodChanged)
                {
                    // If a redirect uses GET but the original request used POST with content, then the redirected
                    // request will fail with an exception.
                    // There's also no way to send content using GET (except in the URL, of course), so discarding
                    // request.Content is what we should do.
                    //
                    // See https://github.com/xamarin/xamarin-android/issues/1282
                    if (redirectState.Method == HttpMethod.Get)
                    {
                        if (Logger.LogNet)
                        {
                            Logger.Log(LogLevel.Info, LOG_APP, $"Discarding content on redirect");
                        }
                        request.Content = null;
                    }
                }

                if (disposeRet)
                {
                    ret.Dispose();
                    ret = null;
                }
                else
                {
                    CopyHeaders(httpConnection, ret);
                    ParseCookies(ret, connectionUri);
                }

                return(ret);
            }

            switch (statusCode)
            {
            case HttpStatusCode.Unauthorized:
            case HttpStatusCode.ProxyAuthenticationRequired:
                // We don't resend the request since that would require new set of credentials if the
                // ones provided in Credentials are invalid (or null) and that, in turn, may require asking the
                // user which is not something that should be taken care of by us and in this
                // context. The application should be responsible for this.
                // HttpClientHandler throws an exception in this instance, but I think it's not a good
                // idea. We'll return the response message with all the information required by the
                // application to fill in the blanks and provide the requested credentials instead.
                //
                // We return the body of the response too, but the Java client will throw
                // a FileNotFound exception if we attempt to access the input stream.
                // Instead we try to read the error stream and return an default message if the error stream isn't readable.
                ret.Content = GetErrorContent(httpConnection, new StringContent("Unauthorized", Encoding.ASCII));
                CopyHeaders(httpConnection, ret);

                if (ret.Headers.WwwAuthenticate != null)
                {
                    ProxyAuthenticationRequested = false;
                    CollectAuthInfo(ret.Headers.WwwAuthenticate);
                }
                else if (ret.Headers.ProxyAuthenticate != null)
                {
                    ProxyAuthenticationRequested = true;
                    CollectAuthInfo(ret.Headers.ProxyAuthenticate);
                }

                ret.RequestedAuthentication = RequestedAuthentication;
                return(ret);
            }

            CopyHeaders(httpConnection, ret);
            ParseCookies(ret, connectionUri);

            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"Returning");
            }
            return(ret);
        }
예제 #7
0
        /// <summary>
        /// Creates, configures and processes an asynchronous request to the indicated resource.
        /// </summary>
        /// <returns>Task in which the request is executed</returns>
        /// <param name="request">Request provided by <see cref="System.Net.Http.HttpClient"/></param>
        /// <param name="cancellationToken">Cancellation token.</param>
        protected override async Task <HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            AssertSelf();
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            if (!request.RequestUri.IsAbsoluteUri)
            {
                throw new ArgumentException("Must represent an absolute URI", "request");
            }

            var redirectState = new RequestRedirectionState {
                NewUrl          = request.RequestUri,
                RedirectCounter = 0,
                Method          = request.Method
            };

            while (true)
            {
                URL           java_url = new URL(EncodeUrl(redirectState.NewUrl));
                URLConnection java_connection;
                if (UseProxy)
                {
                    java_connection = java_url.OpenConnection();
                }
                else
                {
                    java_connection = java_url.OpenConnection(Java.Net.Proxy.NoProxy);
                }

                var httpsConnection = java_connection as HttpsURLConnection;
                if (httpsConnection != null)
                {
                    IHostnameVerifier hnv = GetSSLHostnameVerifier(httpsConnection);
                    if (hnv != null)
                    {
                        httpsConnection.HostnameVerifier = hnv;
                    }
                }

                if (ConnectTimeout != TimeSpan.Zero)
                {
                    java_connection.ConnectTimeout = checked ((int)ConnectTimeout.TotalMilliseconds);
                }

                if (ReadTimeout != TimeSpan.Zero)
                {
                    java_connection.ReadTimeout = checked ((int)ReadTimeout.TotalMilliseconds);
                }

                HttpURLConnection   httpConnection = await SetupRequestInternal(request, java_connection).ConfigureAwait(continueOnCapturedContext: false);;
                HttpResponseMessage response       = await ProcessRequest(request, java_url, httpConnection, cancellationToken, redirectState).ConfigureAwait(continueOnCapturedContext: false);;
                if (response != null)
                {
                    return(response);
                }

                if (redirectState.NewUrl == null)
                {
                    throw new InvalidOperationException("Request redirected but no new URI specified");
                }
                request.Method = redirectState.Method;
            }
        }
        bool HandleRedirect(HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet)
        {
            if (!AllowAutoRedirect)
            {
                disposeRet = false;
                return(true);                // We shouldn't follow and there's no data to fetch, just return
            }
            disposeRet = true;

            redirectState.NewUrl = null;
            switch (redirectCode)
            {
            case HttpStatusCode.MultipleChoices:                       // 300
                break;

            case HttpStatusCode.Moved:                                 // 301
            case HttpStatusCode.Redirect:                              // 302
            case HttpStatusCode.SeeOther:                              // 303
                redirectState.Method = HttpMethod.Get;
                break;

            case HttpStatusCode.TemporaryRedirect:                     // 307
                break;

            default:
                if ((int)redirectCode >= 300 && (int)redirectCode < 400)
                {
                    throw new InvalidOperationException($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported");
                }
                return(false);
            }

            IDictionary <string, IList <string> > headers = httpConnection.HeaderFields;
            IList <string> locationHeader;

            if (!headers.TryGetValue("Location", out locationHeader) || locationHeader == null || locationHeader.Count == 0)
            {
                throw new InvalidOperationException($"HTTP connection redirected with code {redirectCode} ({(int)redirectCode}) but no Location header found in response");
            }

            if (locationHeader.Count > 1 && Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"More than one location header for HTTP {redirectCode} redirect. Will use the first one.");
            }

            redirectState.RedirectCounter++;
            if (redirectState.RedirectCounter >= MaxAutomaticRedirections)
            {
                throw new WebException($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)");
            }

            Uri location = new Uri(locationHeader [0], UriKind.Absolute);

            redirectState.NewUrl = location;
            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Debug, LOG_APP, $"Request redirected to {location}");
            }

            return(true);
        }
        async Task <HttpResponseMessage> DoProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
        {
            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"{this}.DoProcessRequest ()");
            }
            try {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"  connecting");
                }
                await httpConnection.ConnectAsync().ConfigureAwait(false);

                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"  connected");
                }
            } catch (Java.Net.ConnectException ex) {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Connection exception {ex}");
                }
                // Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler
                throw new WebException(ex.Message, ex, WebExceptionStatus.ConnectFailure, null);
            }

            if (httpConnection.DoOutput)
            {
                await request.Content.CopyToAsync(httpConnection.OutputStream).ConfigureAwait(false);
            }

            var statusCode    = (HttpStatusCode)httpConnection.ResponseCode;
            var connectionUri = new Uri(httpConnection.URL.ToString());

            // If the request was redirected we need to put the new URL in the request
            request.RequestUri = connectionUri;
            var ret = new AndroidHttpResponseMessage(javaUrl, httpConnection)
            {
                RequestMessage = request,
                ReasonPhrase   = httpConnection.ResponseMessage,
                StatusCode     = statusCode,
            };

            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"Status code: {statusCode}");
            }

            bool disposeRet;

            if (HandleRedirect(statusCode, httpConnection, redirectState, out disposeRet))
            {
                if (disposeRet)
                {
                    ret.Dispose();
                    ret = null;
                }
                return(ret);
            }

            switch (statusCode)
            {
            case HttpStatusCode.Unauthorized:
            case HttpStatusCode.ProxyAuthenticationRequired:
                // We don't resend the request since that would require new set of credentials if the
                // ones provided in Credentials are invalid (or null) and that, in turn, may require asking the
                // user which is not something that should be taken care of by us and in this
                // context. The application should be responsible for this.
                // HttpClientHandler throws an exception in this instance, but I think it's not a good
                // idea. We'll return the response message with all the information required by the
                // application to fill in the blanks and provide the requested credentials instead.
                //
                // We should return the body of the response too but, alas, the Java client will throw
                // a, wait for it, FileNotFound exception if we attempt to access the input stream. So
                // no body, just a dummy. Java FTW!
                ret.Content = new StringContent("Unauthorized", Encoding.ASCII);
                CopyHeaders(httpConnection, ret);

                if (ret.Headers.WwwAuthenticate != null)
                {
                    ProxyAuthenticationRequested = false;
                    CollectAuthInfo(ret.Headers.WwwAuthenticate);
                }
                else if (ret.Headers.ProxyAuthenticate != null)
                {
                    ProxyAuthenticationRequested = true;
                    CollectAuthInfo(ret.Headers.ProxyAuthenticate);
                }

                ret.RequestedAuthentication = RequestedAuthentication;
                return(ret);
            }

            if (!IsErrorStatusCode(statusCode))
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Reading...");
                }
                Stream inputStream = new BufferedStream(httpConnection.InputStream);
                if (decompress_here)
                {
                    string[] encodings = httpConnection.ContentEncoding?.Split(',');
                    if (encodings != null)
                    {
                        if (encodings.Contains(GZIP_ENCODING, StringComparer.OrdinalIgnoreCase))
                        {
                            inputStream = new GZipStream(inputStream, CompressionMode.Decompress);
                        }
                        else if (encodings.Contains(DEFLATE_ENCODING, StringComparer.OrdinalIgnoreCase))
                        {
                            inputStream = new DeflateStream(inputStream, CompressionMode.Decompress);
                        }
                    }
                }
                ret.Content = new StreamContent(inputStream);
            }
            else
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Status code is {statusCode}, returning empty content");
                }
                // For 400 >= response code <= 599 the Java client throws the FileNotFound exeption when attempting to read from the connection
                // Client tests require we return no content here
                ret.Content = new StringContent(String.Empty, Encoding.ASCII);
            }
            CopyHeaders(httpConnection, ret);

            IEnumerable <string> cookieHeaderValue;

            if (!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null)
            {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"No cookies");
                }
                return(ret);
            }

            try {
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Parsing cookies");
                }
                CookieContainer.SetCookies(connectionUri, String.Join(",", cookieHeaderValue));
            } catch (Exception ex) {
                // We don't want to terminate the response because of a bad cookie, hence just reporting
                // the issue. We might consider adding a virtual method to let the user handle the
                // issue, but not sure if it's really needed. Set-Cookie header will be part of the
                // header collection so the user can always examine it if they spot an error.
                if (Logger.LogNet)
                {
                    Logger.Log(LogLevel.Info, LOG_APP, $"Failed to parse cookies in the server response. {ex.GetType ()}: {ex.Message}");
                }
            }

            if (Logger.LogNet)
            {
                Logger.Log(LogLevel.Info, LOG_APP, $"Returning");
            }
            return(ret);
        }