From e147e1c1d2ec44b5ecd510ec7fb756ac7108bc22 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Sun, 9 Apr 2023 14:48:05 +0200 Subject: [PATCH] Add CacheControl (#2053) Cleanup response writer code --- .../Extensions/HttpResponseExtensions.cs | 53 +++++++++++++++++-- src/RestSharp/Extensions/StreamExtensions.cs | 6 +-- src/RestSharp/Properties/IsExternalInit.cs | 2 +- src/RestSharp/Request/RestRequest.cs | 14 +++-- src/RestSharp/Response/ResponseHandling.cs | 45 ---------------- src/RestSharp/Response/RestResponse.cs | 20 ++----- src/RestSharp/RestClient.Async.cs | 14 ++--- src/RestSharp/RestClient.Extensions.cs | 6 +-- .../DownloadFileTests.cs | 31 ++++++----- .../RedirectTests.cs | 46 ++++++++++++++++ .../Server/TestServer.cs | 1 + test/RestSharp.Tests/OptionsTests.cs | 16 ++++++ 12 files changed, 153 insertions(+), 101 deletions(-) delete mode 100644 src/RestSharp/Response/ResponseHandling.cs create mode 100644 test/RestSharp.Tests.Integrated/RedirectTests.cs create mode 100644 test/RestSharp.Tests/OptionsTests.cs diff --git a/src/RestSharp/Extensions/HttpResponseExtensions.cs b/src/RestSharp/Extensions/HttpResponseExtensions.cs index 4558f2d7a..734ff0cd2 100644 --- a/src/RestSharp/Extensions/HttpResponseExtensions.cs +++ b/src/RestSharp/Extensions/HttpResponseExtensions.cs @@ -13,15 +13,60 @@ // limitations under the License. // +using System.Text; + namespace RestSharp.Extensions; -public static class HttpResponseExtensions { - internal static Exception? MaybeException(this HttpResponseMessage httpResponse) +static class HttpResponseExtensions { + public static Exception? MaybeException(this HttpResponseMessage httpResponse) => httpResponse.IsSuccessStatusCode ? null -#if NETSTANDARD || NETFRAMEWORK +#if NET + : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); +#else : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}"); +#endif + + public static string GetResponseString(this HttpResponseMessage response, byte[] bytes, Encoding clientEncoding) { + var encodingString = response.Content.Headers.ContentType?.CharSet; + var encoding = encodingString != null ? TryGetEncoding(encodingString) : clientEncoding; + + using var reader = new StreamReader(new MemoryStream(bytes), encoding); + return reader.ReadToEnd(); + + Encoding TryGetEncoding(string es) { + try { + return Encoding.GetEncoding(es); + } + catch { + return Encoding.Default; + } + } + } + + public static Task ReadResponseStream( + this HttpResponseMessage httpResponse, + Func? writer, + CancellationToken cancellationToken = default + ) { + var readTask = writer == null ? ReadResponse() : ReadAndConvertResponse(); + return readTask; + + Task ReadResponse() { +#if NET + return httpResponse.Content.ReadAsStreamAsync(cancellationToken)!; +# else + return httpResponse.Content.ReadAsStreamAsync(); +#endif + } + + async Task ReadAndConvertResponse() { +#if NET + await using var original = await ReadResponse().ConfigureAwait(false); #else - : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); + using var original = await ReadResponse().ConfigureAwait(false); #endif + return writer!(original!); + } + } } diff --git a/src/RestSharp/Extensions/StreamExtensions.cs b/src/RestSharp/Extensions/StreamExtensions.cs index 430437080..fd5cf685b 100644 --- a/src/RestSharp/Extensions/StreamExtensions.cs +++ b/src/RestSharp/Extensions/StreamExtensions.cs @@ -30,10 +30,10 @@ public static async Task ReadAsBytes(this Stream input, CancellationToke using var ms = new MemoryStream(); int read; -#if NETSTANDARD || NETFRAMEWORK - while ((read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) -#else +#if NET while ((read = await input.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) +#else + while ((read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) #endif ms.Write(buffer, 0, read); diff --git a/src/RestSharp/Properties/IsExternalInit.cs b/src/RestSharp/Properties/IsExternalInit.cs index 4f3c65f81..a77ccc3c3 100644 --- a/src/RestSharp/Properties/IsExternalInit.cs +++ b/src/RestSharp/Properties/IsExternalInit.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD || NETFRAMEWORK +#if !NET using System.ComponentModel; // ReSharper disable once CheckNamespace diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index bf70b4f92..6e5290595 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -13,6 +13,7 @@ // limitations under the License. using System.Net; +using System.Net.Http.Headers; using RestSharp.Authenticators; using RestSharp.Extensions; @@ -25,8 +26,8 @@ namespace RestSharp; /// Container for data used to make requests /// public class RestRequest { - readonly Func? _advancedResponseHandler; - readonly Func? _responseWriter; + Func? _advancedResponseHandler; + Func? _responseWriter; /// /// Default constructor @@ -186,12 +187,17 @@ public RestRequest(Uri resource, Method method = Method.Get) /// public HttpCompletionOption CompletionOption { get; set; } = HttpCompletionOption.ResponseContentRead; + /// + /// Cache policy to be used for requests using + /// + public CacheControlHeaderValue? CachePolicy { get; set; } + /// /// Set this to write response to Stream rather than reading into memory. /// public Func? ResponseWriter { get => _responseWriter; - init { + set { if (AdvancedResponseWriter != null) throw new ArgumentException( "AdvancedResponseWriter is not null. Only one response writer can be used." @@ -206,7 +212,7 @@ public RestRequest(Uri resource, Method method = Method.Get) /// public Func? AdvancedResponseWriter { get => _advancedResponseHandler; - init { + set { if (ResponseWriter != null) throw new ArgumentException("ResponseWriter is not null. Only one response writer can be used."); _advancedResponseHandler = value; diff --git a/src/RestSharp/Response/ResponseHandling.cs b/src/RestSharp/Response/ResponseHandling.cs deleted file mode 100644 index 43e0f4c56..000000000 --- a/src/RestSharp/Response/ResponseHandling.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Text; - -namespace RestSharp; - -static class ResponseHandling { - public static string GetResponseString(this HttpResponseMessage response, byte[] bytes, Encoding clientEncoding) { - var encodingString = response.Content.Headers.ContentType?.CharSet; - var encoding = encodingString != null ? TryGetEncoding(encodingString) : clientEncoding; - - using var reader = new StreamReader(new MemoryStream(bytes), encoding); - return reader.ReadToEnd(); - - Encoding TryGetEncoding(string es) { - try { - return Encoding.GetEncoding(es); - } - catch { - return Encoding.Default; - } - } - } - - public static Task ReadResponse(this HttpResponseMessage response, CancellationToken cancellationToken) { -#if NETSTANDARD || NETFRAMEWORK - return response.Content.ReadAsStreamAsync(); -# else - return response.Content.ReadAsStreamAsync(cancellationToken)!; -#endif - } -} \ No newline at end of file diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index 5f07e38d9..00b704a13 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -72,14 +72,13 @@ CancellationToken cancellationToken return request.AdvancedResponseWriter?.Invoke(httpResponse, request) ?? await GetDefaultResponse().ConfigureAwait(false); async Task GetDefaultResponse() { - var readTask = request.ResponseWriter == null ? ReadResponse() : ReadAndConvertResponse(); -#if NETSTANDARD || NETFRAMEWORK - using var stream = await readTask.ConfigureAwait(false); +#if NET + await using var stream = await httpResponse.ReadResponseStream(request.ResponseWriter, cancellationToken).ConfigureAwait(false); #else - await using var stream = await readTask.ConfigureAwait(false); + using var stream = await httpResponse.ReadResponseStream(request.ResponseWriter, cancellationToken).ConfigureAwait(false); #endif - var bytes = request.ResponseWriter != null || stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false); + var bytes = stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false); var content = bytes == null ? null : httpResponse.GetResponseString(bytes, encoding); return new RestResponse(request) { @@ -101,17 +100,6 @@ async Task GetDefaultResponse() { Cookies = cookieCollection, RootElement = request.RootElement }; - - Task ReadResponse() => httpResponse.ReadResponse(cancellationToken); - - async Task ReadAndConvertResponse() { -#if NETSTANDARD || NETFRAMEWORK - using var original = await ReadResponse().ConfigureAwait(false); -#else - await using var original = await ReadResponse().ConfigureAwait(false); -#endif - return request.ResponseWriter!(original!); - } } } diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 821f4f487..b0cc3f671 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -13,6 +13,7 @@ // limitations under the License. using System.Net; +using System.Net.Http.Headers; using RestSharp.Extensions; namespace RestSharp; @@ -52,16 +53,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo if (response.ResponseMessage == null) return null; - if (request.ResponseWriter != null) { -#if NETSTANDARD || NETFRAMEWORK - using var stream = await response.ResponseMessage.ReadResponse(cancellationToken).ConfigureAwait(false); -#else - await using var stream = await response.ResponseMessage.ReadResponse(cancellationToken).ConfigureAwait(false); -#endif - return request.ResponseWriter(stream!); - } - - return await response.ResponseMessage.ReadResponse(cancellationToken).ConfigureAwait(false); + return await response.ResponseMessage.ReadResponseStream(request.ResponseWriter, cancellationToken).ConfigureAwait(false); } static RestResponse GetErrorResponse(RestRequest request, Exception exception, CancellationToken timeoutToken) { @@ -95,7 +87,7 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo var url = this.BuildUri(request); var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() }; message.Headers.Host = Options.BaseHost; - message.Headers.CacheControl = Options.CachePolicy; + message.Headers.CacheControl = request.CachePolicy ?? Options.CachePolicy; using var timeoutCts = new CancellationTokenSource(request.Timeout > 0 ? request.Timeout : int.MaxValue); using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); diff --git a/src/RestSharp/RestClient.Extensions.cs b/src/RestSharp/RestClient.Extensions.cs index 1ed035160..d1ebbd11e 100644 --- a/src/RestSharp/RestClient.Extensions.cs +++ b/src/RestSharp/RestClient.Extensions.cs @@ -294,10 +294,10 @@ public static async Task DeleteAsync(this IRestClient client, Rest /// The downloaded file. [PublicAPI] public static async Task DownloadDataAsync(this IRestClient client, RestRequest request, CancellationToken cancellationToken = default) { -#if NETSTANDARD || NETFRAMEWORK - using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); -#else +#if NET await using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); +#else + using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); #endif return stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false); } diff --git a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs index c9ad6d229..f2cd59482 100644 --- a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs +++ b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs @@ -34,13 +34,15 @@ void FileHandler(HttpListenerRequest request, HttpListenerResponse response) { public async Task AdvancedResponseWriter_without_ResponseWriter_reads_stream() { var tag = string.Empty; - var rr = new RestRequest("Assets/Koala.jpg") { - AdvancedResponseWriter = (response, request) => { - var buf = new byte[16]; - response.Content.ReadAsStream().Read(buf, 0, buf.Length); - tag = Encoding.ASCII.GetString(buf, 6, 4); - return new RestResponse(request); - } + // ReSharper disable once UseObjectOrCollectionInitializer + var rr = new RestRequest("Assets/Koala.jpg"); + + rr.AdvancedResponseWriter = (response, request) => { + var buf = new byte[16]; + // ReSharper disable once MustUseReturnValue + response.Content.ReadAsStream().Read(buf, 0, buf.Length); + tag = Encoding.ASCII.GetString(buf, 6, 4); + return new RestResponse(request); }; await _client.ExecuteAsync(rr); @@ -50,7 +52,7 @@ public async Task AdvancedResponseWriter_without_ResponseWriter_reads_stream() { [Fact] public async Task Handles_File_Download_Failure() { var request = new RestRequest("Assets/Koala1.jpg"); - var task = () => _client.DownloadDataAsync(request); + var task = () => _client.DownloadDataAsync(request); await task.Should().ThrowAsync().WithMessage("Request failed with status code NotFound"); } @@ -67,13 +69,14 @@ public async Task Handles_Binary_File_Download() { public async Task Writes_Response_To_Stream() { var tempFile = Path.GetTempFileName(); - var request = new RestRequest("Assets/Koala.jpg") { - ResponseWriter = responseStream => { - using var writer = File.OpenWrite(tempFile); + // ReSharper disable once UseObjectOrCollectionInitializer + var request = new RestRequest("Assets/Koala.jpg"); - responseStream.CopyTo(writer); - return null; - } + request.ResponseWriter = responseStream => { + using var writer = File.OpenWrite(tempFile); + + responseStream.CopyTo(writer); + return null; }; var response = await _client.DownloadDataAsync(request); diff --git a/test/RestSharp.Tests.Integrated/RedirectTests.cs b/test/RestSharp.Tests.Integrated/RedirectTests.cs new file mode 100644 index 000000000..11b304808 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RedirectTests.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Net; +using RestSharp.Tests.Integrated.Server; + +namespace RestSharp.Tests.Integrated; + +[Collection(nameof(TestServerCollection))] +public class RedirectTests { + readonly RestClient _client; + + public RedirectTests(TestServerFixture fixture, ITestOutputHelper output) { + var options = new RestClientOptions(fixture.Server.Url) { + FollowRedirects = true + }; + _client = new RestClient(options); + } + + [Fact] + public async Task Can_Perform_GET_Async_With_Redirect() { + const string val = "Works!"; + + var request = new RestRequest("redirect"); + + var response = await _client.ExecuteAsync(request); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be(val); + } + + class Response { + public string? Message { get; set; } + } +} diff --git a/test/RestSharp.Tests.Integrated/Server/TestServer.cs b/test/RestSharp.Tests.Integrated/Server/TestServer.cs index 6b34ad7d8..dd075532a 100644 --- a/test/RestSharp.Tests.Integrated/Server/TestServer.cs +++ b/test/RestSharp.Tests.Integrated/Server/TestServer.cs @@ -40,6 +40,7 @@ public HttpServer(ITestOutputHelper? output = null) { // Cookies _app.MapGet("get-cookies", CookieHandlers.HandleCookies); _app.MapGet("set-cookies", CookieHandlers.HandleSetCookies); + _app.MapGet("redirect", () => Results.Redirect("/success", false, true)); // PUT _app.MapPut( diff --git a/test/RestSharp.Tests/OptionsTests.cs b/test/RestSharp.Tests/OptionsTests.cs new file mode 100644 index 000000000..ea8acb5c7 --- /dev/null +++ b/test/RestSharp.Tests/OptionsTests.cs @@ -0,0 +1,16 @@ +namespace RestSharp.Tests; + +public class OptionsTests { + [Fact] + public void Ensure_follow_redirect() { + var value = false; + var options = new RestClientOptions { FollowRedirects = true, ConfigureMessageHandler = Configure}; + var _ = new RestClient(options); + value.Should().BeTrue(); + + HttpMessageHandler Configure(HttpMessageHandler handler) { + value = ((handler as HttpClientHandler)!).AllowAutoRedirect; + return handler; + } + } +}