Index: Src/GoogleApis.Tests/Apis/Download/MediaDownloaderTest.cs =================================================================== --- a/Src/GoogleApis.Tests/Apis/Download/MediaDownloaderTest.cs +++ b/Src/GoogleApis.Tests/Apis/Download/MediaDownloaderTest.cs @@ -129,6 +129,14 @@ Subtest_Download_Chunks(100); } + + /// Tests that download works in case the URI download contains query parameters. + [Test] + public void Download_SingleChunk_UriContainsQueryParameters() + { + Subtest_Download_Chunks((int)StreamContent.Length, true, 0, "https://www.sample.com?a=1&b=2"); + } + /// /// Tests that download asynchronously works in case the server returns multiple chunks to the client. /// @@ -165,15 +173,17 @@ /// The chunk size for each part. /// Indicates if this download should be synchronously or asynchronously. /// Defines the request index to cancel the download request. - private void Subtest_Download_Chunks(int chunkSize, bool sync = true, int cancelRequest = 0) + /// + private void Subtest_Download_Chunks(int chunkSize, bool sync = true, int cancelRequest = 0, + string downloadUri = "http://www.sample.com") { // reset the steam StreamContent.Position = 0; - string downloadUri = "http://www.sample.com"; var handler = new MultipleChunksMessageHandler(); handler.ChunkSize = chunkSize; - handler.DownloadUri = new Uri(downloadUri + "?alt=media"); + handler.DownloadUri = new Uri(downloadUri + + (downloadUri.Contains("?") ? "&" : "?") + "alt=media"); // support cancellation if (cancelRequest > 0) Index: Src/GoogleApis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs =================================================================== --- a/Src/GoogleApis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs +++ b/Src/GoogleApis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs @@ -118,7 +118,7 @@ var redirectHandler = new RedirectMessageHandler(location); var configurableHanlder = new ConfigurableMessageHandler(redirectHandler) { - NumTries = 8 + NumRedirects = 5 }; using (var client = new HttpClient(configurableHanlder)) { @@ -131,8 +131,9 @@ HttpResponseMessage response = client.SendAsync(request).Result; Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect)); - Assert.That(response.Headers.Location, Is.EqualTo(new Uri(location + configurableHanlder.NumTries))); - Assert.That(redirectHandler.Calls, Is.EqualTo(configurableHanlder.NumTries)); + Assert.That(response.Headers.Location, Is.EqualTo( + new Uri(location + (configurableHanlder.NumRedirects + 1)))); + Assert.That(redirectHandler.Calls, Is.EqualTo(configurableHanlder.NumRedirects + 1)); } } Index: Src/GoogleApis.Tests/Apis/Requests/ClientServiceRequestTest.cs =================================================================== --- a/Src/GoogleApis.Tests/Apis/Requests/ClientServiceRequestTest.cs +++ b/Src/GoogleApis.Tests/Apis/Requests/ClientServiceRequestTest.cs @@ -401,16 +401,16 @@ { // we expect a task canceled exception in case the canceled request is less or equal total // number of retries - Assert.False(cancelRequestNum > service.HttpClient.MessageHandler.NumTries); + Assert.False(cancelRequestNum > service.HttpClient.MessageHandler.NumRedirects + 1); } else { // exception should be thrown as a result of casting to MockResponse object - Assert.True(cancelRequestNum > service.HttpClient.MessageHandler.NumTries); + Assert.True(cancelRequestNum > service.HttpClient.MessageHandler.NumRedirects + 1); } } - var expectedCalls = Math.Min(service.HttpClient.MessageHandler.NumTries, cancelRequestNum); + var expectedCalls = Math.Min(service.HttpClient.MessageHandler.NumRedirects + 1, cancelRequestNum); Assert.That(handler.Calls, Is.EqualTo(expectedCalls)); } } Index: Src/GoogleApis/Apis/Http/ConfigurableMessageHandler.cs =================================================================== --- a/Src/GoogleApis/Apis/Http/ConfigurableMessageHandler.cs +++ b/Src/GoogleApis/Apis/Http/ConfigurableMessageHandler.cs @@ -92,6 +92,11 @@ /// or which handles the /// abnormal Http response or exception, before being terminated. /// Set 1 for not retrying requests. The default value is 3". + /// + /// The number of allowed redirects (3xx) is defined by . This property defines + /// only the allowed tries for >=400 responses, or in a case an exception was thrown. For example you set + /// to 1 and to 5, a retry will happen only for 5 redirects. + /// /// public int NumTries { @@ -106,6 +111,26 @@ } } + /// Number of redirects allowed. Default is 10. + private int numRedirect = 10; + + /// + /// Gets or sets the number of redirects that will be allowed to execute. The default value is 10. + /// See for more information. + /// + public int NumRedirects + { + get { return numRedirect; } + set + { + if (value > MaxAllowedNumTries || value < 1) + { + throw new ArgumentOutOfRangeException("NumRedirects"); + } + numRedirect = value; + } + } + /// /// Gets or sets whether the handler should follow a redirect when a redirect response is received. Default /// value is true. @@ -138,6 +163,8 @@ var loggable = IsLoggingEnabled && Logger.IsDebugEnabled; int triesRemaining = NumTries; + int redirectRemaining = NumRedirects; + Exception lastException = null; // set User-Agent header @@ -177,7 +204,10 @@ } // decrease the number of retries - triesRemaining--; + if (response == null || (int)response.StatusCode >= 400) + { + triesRemaining--; + } // exception was thrown , try to handle it if (response == null) @@ -237,6 +267,11 @@ { if (HandleRedirect(response)) { + if (redirectRemaining-- == 0) + { + triesRemaining = 0; + } + errorHandled = true; if (loggable) { Index: Src/GoogleApis/Apis/[Media]/Download/MediaDownloader.cs =================================================================== --- a/Src/GoogleApis/Apis/[Media]/Download/MediaDownloader.cs +++ b/Src/GoogleApis/Apis/[Media]/Download/MediaDownloader.cs @@ -15,6 +15,7 @@ */ using System; +using System.Linq; using System.IO; using System.Net.Http.Headers; using System.Threading; @@ -189,7 +190,26 @@ throw new ArgumentException("stream doesn't support write operations"); } - var builder = new RequestBuilder() { BaseUri = new Uri(url) }; + RequestBuilder builder = null; + + var uri = new Uri(url); + if (string.IsNullOrEmpty(uri.Query)) + { + builder = new RequestBuilder() { BaseUri = new Uri(url) }; + } + else + { + builder = new RequestBuilder() { BaseUri = new Uri(url.Substring(0, url.IndexOf("?"))) }; + // remove '?' at the beginning + var query = uri.Query.Substring(1); + var pairs = from parameter in query.Split('&') + select parameter.Split('='); + // add each query parameter. each pair contains the key [0] and then its value [1] + foreach (var p in pairs) + { + builder.AddParameter(RequestParameterType.Query, p[0], p[1]); + } + } builder.AddParameter(RequestParameterType.Query, "alt", "media"); long currentRequestFirstBytePos = 0; @@ -219,8 +239,20 @@ // read the headers and check if all the media content was already downloaded var contentRange = response.Content.Headers.ContentRange; - currentRequestFirstBytePos = contentRange.To.Value + 1; - long mediaContentLength = contentRange.Length.Value; + long mediaContentLength; + + if (contentRange == null) + { + // content range is null when the server doesn't adhere the media download protocol, in + // that case we got all the media in one chunk + currentRequestFirstBytePos = mediaContentLength = + response.Content.Headers.ContentLength.Value; + } + else + { + currentRequestFirstBytePos = contentRange.To.Value + 1; + mediaContentLength = contentRange.Length.Value; + } if (currentRequestFirstBytePos == mediaContentLength) {