From 95767d472c7b4bc983828d14e8b4ee4d4f23d4aa Mon Sep 17 00:00:00 2001 From: edobbsskylark <116915471+edobbsskylark@users.noreply.github.com> Date: Mon, 7 Nov 2022 09:55:48 -0600 Subject: [PATCH 01/49] Token issues (#1955) While following this page and using it a lot while learning APIs, I had an issue where every request I was making to my client, it would reach out to get a new token. I assume this is not intended, and this was my fix for it. The local variable 'token' in the GetAuthenticationParameter function doesn't live outside of this and there for the obtained bearer token is never saved to the client object. I just forced the gettoken function to save to the Token variable that is in AuthenticatorBase. Please let me know if I am missing something as I would love to learn! I do figure that this does not take into account "what if the bearer token expires?" And to be honest I have no clue how this is handled! I plan on doing some checks if the request comes back with unauthorized, to get a new token, but if you have ideas or know of better ways to handle bearer tokens expiring please let me know! I am new to working with APIs. --- docs/usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index d19374204..a6857a287 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -48,8 +48,8 @@ public class TwitterAuthenticator : AuthenticatorBase { } protected override async ValueTask GetAuthenticationParameter(string accessToken) { - var token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; - return new HeaderParameter(KnownHeaders.Authorization, token); + Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + return new HeaderParameter(KnownHeaders.Authorization, Token); } } ``` From ef6808805ee3d6cf2f2e2fa36730c7e6b4a96709 Mon Sep 17 00:00:00 2001 From: nivmeshorer <116564679+nivmeshorer@users.noreply.github.com> Date: Mon, 7 Nov 2022 17:56:43 +0200 Subject: [PATCH 02/49] Support constructing JwtAuthenticator with token includes Bearer prefix predefined (#1949) Co-authored-by: Niv Meshorer --- src/RestSharp/Authenticators/JwtAuthenticator.cs | 2 +- test/RestSharp.Tests/JwtAuthTests.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/RestSharp/Authenticators/JwtAuthenticator.cs b/src/RestSharp/Authenticators/JwtAuthenticator.cs index 41479d2f1..10e8493df 100644 --- a/src/RestSharp/Authenticators/JwtAuthenticator.cs +++ b/src/RestSharp/Authenticators/JwtAuthenticator.cs @@ -28,7 +28,7 @@ public JwtAuthenticator(string accessToken) : base(GetToken(accessToken)) { } [PublicAPI] public void SetBearerToken(string accessToken) => Token = GetToken(accessToken); - static string GetToken(string accessToken) => $"Bearer {Ensure.NotEmpty(accessToken, nameof(accessToken))}"; + static string GetToken(string accessToken) => Ensure.NotEmpty(accessToken, nameof(accessToken)).StartsWith("Bearer ") ? accessToken : $"Bearer {accessToken}"; protected override ValueTask GetAuthenticationParameter(string accessToken) => new(new HeaderParameter(KnownHeaders.Authorization, accessToken)); diff --git a/test/RestSharp.Tests/JwtAuthTests.cs b/test/RestSharp.Tests/JwtAuthTests.cs index 2d1200e0c..d82a5e137 100644 --- a/test/RestSharp.Tests/JwtAuthTests.cs +++ b/test/RestSharp.Tests/JwtAuthTests.cs @@ -34,6 +34,20 @@ public async Task Can_Set_ValidFormat_Auth_Header() { Assert.True(authParam.Type == ParameterType.HttpHeader); Assert.Equal(_expectedAuthHeaderContent, authParam.Value); } + + [Fact] + public async Task Can_Set_ValidFormat_Auth_Header_With_Bearer_Prefix() { + var client = new RestClient { Authenticator = new JwtAuthenticator($"Bearer {_testJwt}") }; + var request = new RestRequest(); + + //In real case client.Execute(request) will invoke Authenticate method + await client.Authenticator.Authenticate(client, request); + + var authParam = request.Parameters.Single(p => p.Name.Equals(KnownHeaders.Authorization, StringComparison.OrdinalIgnoreCase)); + + Assert.True(authParam.Type == ParameterType.HttpHeader); + Assert.Equal(_expectedAuthHeaderContent, authParam.Value); + } [Fact] public async Task Check_Only_Header_Authorization() { From 50d0f8a22611918aba6910e5d2ea2ce57a0c45ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 17:01:33 +0100 Subject: [PATCH 03/49] Bump actions/setup-dotnet from 2 to 3 (#1939) Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 2 to 3. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-dev.yml | 2 +- .github/workflows/pull-request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 566b807e0..f8918b898 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: '6.0' - diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 124a43967..f5e76ddaf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: '6.0' - From e968fa8f57dbd581857b14338f88dd5ca9badbb8 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 8 Nov 2022 10:59:08 +0100 Subject: [PATCH 04/49] Update .NET version in Actions --- .github/workflows/build-dev.yml | 2 +- .github/workflows/pull-request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index f8918b898..1e95432e0 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -27,7 +27,7 @@ jobs: name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0' + dotnet-version: '7.0' - name: Unshallow run: git fetch --prune --unshallow diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f5e76ddaf..c566a4cc8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -17,7 +17,7 @@ jobs: name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0' + dotnet-version: '7.0' - name: Run tests run: dotnet test -c Release From d2c33abca70279d337662adec3b73cf09ec539ca Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 8 Nov 2022 13:08:35 +0100 Subject: [PATCH 05/49] Added .NET Framework 471 as a target. .NET 5 is out --- src/Directory.Build.props | 2 +- .../RestSharp.Serializers.CsvHelper.csproj | 3 +++ .../RestSharp.Serializers.NewtonsoftJson.csproj | 1 + .../RestSharp.Serializers.Xml.csproj | 3 +++ src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs | 2 +- src/RestSharp/Extensions/StreamExtensions.cs | 2 +- src/RestSharp/Properties/IsExternalInit.cs | 2 +- src/RestSharp/Response/ResponseHandling.cs | 2 +- src/RestSharp/Response/RestResponse.cs | 8 ++++---- src/RestSharp/RestClient.Async.cs | 4 ++-- src/RestSharp/RestClientExtensions.cs | 4 ++-- src/RestSharp/RestClientOptions.cs | 2 +- src/RestSharp/RestSharp.csproj | 6 +++++- 13 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c99c4fe4a..7b6217a89 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - netstandard2.0;net5.0;net6.0 + netstandard2.0;net471;net6.0;net7.0 restsharp.png Apache-2.0 https://restsharp.dev diff --git a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj index 7d50d166b..2168d422a 100644 --- a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj +++ b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj @@ -5,4 +5,7 @@ + + + diff --git a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj index f93532064..260fc6fe4 100644 --- a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj +++ b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj @@ -7,5 +7,6 @@ + diff --git a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj index aa523d350..1d6c8eaab 100644 --- a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj +++ b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj @@ -5,4 +5,7 @@ + + + diff --git a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs index 14f743d81..05e515f72 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs @@ -263,7 +263,7 @@ string GetAuthorizationHeader() { if (!Realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm!)}\""); - return "OAuth " + string.Join(",", oathParameters); + return $"OAuth {string.Join(",", oathParameters)}"; } } } diff --git a/src/RestSharp/Extensions/StreamExtensions.cs b/src/RestSharp/Extensions/StreamExtensions.cs index b007b1c83..430437080 100644 --- a/src/RestSharp/Extensions/StreamExtensions.cs +++ b/src/RestSharp/Extensions/StreamExtensions.cs @@ -30,7 +30,7 @@ public static async Task ReadAsBytes(this Stream input, CancellationToke using var ms = new MemoryStream(); int read; -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK while ((read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) #else while ((read = await input.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) diff --git a/src/RestSharp/Properties/IsExternalInit.cs b/src/RestSharp/Properties/IsExternalInit.cs index deb2fb270..4f3c65f81 100644 --- a/src/RestSharp/Properties/IsExternalInit.cs +++ b/src/RestSharp/Properties/IsExternalInit.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK using System.ComponentModel; // ReSharper disable once CheckNamespace diff --git a/src/RestSharp/Response/ResponseHandling.cs b/src/RestSharp/Response/ResponseHandling.cs index a22fa05c0..43e0f4c56 100644 --- a/src/RestSharp/Response/ResponseHandling.cs +++ b/src/RestSharp/Response/ResponseHandling.cs @@ -36,7 +36,7 @@ Encoding TryGetEncoding(string es) { } public static Task ReadResponse(this HttpResponseMessage response, CancellationToken cancellationToken) { -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK return response.Content.ReadAsStreamAsync(); # else return response.Content.ReadAsStreamAsync(cancellationToken)!; diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index 503d2c7f2..35f560a3a 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -58,7 +58,7 @@ public static RestResponse FromResponse(RestResponse response) /// /// Container for data sent back from API /// -[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "()}")] +[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] public class RestResponse : RestResponseBase { internal static async Task FromHttpResponse( HttpResponseMessage httpResponse, @@ -72,7 +72,7 @@ CancellationToken cancellationToken async Task GetDefaultResponse() { var readTask = request.ResponseWriter == null ? ReadResponse() : ReadAndConvertResponse(); -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK using var stream = await readTask.ConfigureAwait(false); #else await using var stream = await readTask.ConfigureAwait(false); @@ -105,7 +105,7 @@ async Task GetDefaultResponse() { Exception? MaybeException() => httpResponse.IsSuccessStatusCode ? null -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}"); #else : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); @@ -114,7 +114,7 @@ async Task GetDefaultResponse() { Task ReadResponse() => httpResponse.ReadResponse(cancellationToken); async Task ReadAndConvertResponse() { -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK using var original = await ReadResponse().ConfigureAwait(false); #else await using var original = await ReadResponse().ConfigureAwait(false); diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 7a353e550..437639477 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -106,7 +106,7 @@ record InternalResponse(HttpResponseMessage? ResponseMessage, Uri Url, Exception if (response.ResponseMessage == null) return null; if (request.ResponseWriter != null) { -#if NETSTANDARD +#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); @@ -138,7 +138,7 @@ static HttpMethod AsHttpMethod(Method method) Method.Delete => HttpMethod.Delete, Method.Head => HttpMethod.Head, Method.Options => HttpMethod.Options, -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK Method.Patch => new HttpMethod("PATCH"), #else Method.Patch => HttpMethod.Patch, diff --git a/src/RestSharp/RestClientExtensions.cs b/src/RestSharp/RestClientExtensions.cs index c91bf10b8..8ebe456bb 100644 --- a/src/RestSharp/RestClientExtensions.cs +++ b/src/RestSharp/RestClientExtensions.cs @@ -294,7 +294,7 @@ public static async Task DeleteAsync(this RestClient client, RestR /// The downloaded file. [PublicAPI] public static async Task DownloadDataAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); #else await using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); @@ -319,7 +319,7 @@ [EnumeratorCancellation] CancellationToken cancellationToken ) { var request = new RestRequest(resource); -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); #else await using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); diff --git a/src/RestSharp/RestClientOptions.cs b/src/RestSharp/RestClientOptions.cs index 43fec3f3a..bc4a15a48 100644 --- a/src/RestSharp/RestClientOptions.cs +++ b/src/RestSharp/RestClientOptions.cs @@ -57,7 +57,7 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba /// public bool DisableCharset { get; set; } -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.GZip; #else public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.All; diff --git a/src/RestSharp/RestSharp.csproj b/src/RestSharp/RestSharp.csproj index 55f34844c..6531042fd 100644 --- a/src/RestSharp/RestSharp.csproj +++ b/src/RestSharp/RestSharp.csproj @@ -1,8 +1,12 @@  - + + + + + From 052357b025da664327747a721a7f978c4c0a124b Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 8 Nov 2022 18:08:26 +0100 Subject: [PATCH 06/49] Let it throw (#1962) --- .../Extensions/HttpResponseExtensions.cs | 27 +++++++++++++++++++ src/RestSharp/Response/RestResponse.cs | 11 +------- src/RestSharp/RestClient.Async.cs | 24 ++++++++--------- .../DownloadFileTests.cs | 12 +++++++-- 4 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 src/RestSharp/Extensions/HttpResponseExtensions.cs diff --git a/src/RestSharp/Extensions/HttpResponseExtensions.cs b/src/RestSharp/Extensions/HttpResponseExtensions.cs new file mode 100644 index 000000000..53b873e6c --- /dev/null +++ b/src/RestSharp/Extensions/HttpResponseExtensions.cs @@ -0,0 +1,27 @@ +// 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. +// + +namespace RestSharp.Extensions; + +public static class HttpResponseExtensions { + internal static Exception? MaybeException(this HttpResponseMessage httpResponse) + => httpResponse.IsSuccessStatusCode + ? null +#if NETSTANDARD + : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}"); +#else + : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); +#endif +} diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index 503d2c7f2..a9225ec23 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -89,7 +89,7 @@ async Task GetDefaultResponse() { ContentLength = httpResponse.Content.Headers.ContentLength, ContentType = httpResponse.Content.Headers.ContentType?.MediaType, ResponseStatus = calculateResponseStatus(httpResponse), - ErrorException = MaybeException(), + ErrorException = httpResponse.MaybeException(), ResponseUri = httpResponse.RequestMessage!.RequestUri, Server = httpResponse.Headers.Server.ToString(), StatusCode = httpResponse.StatusCode, @@ -102,15 +102,6 @@ async Task GetDefaultResponse() { RootElement = request.RootElement }; - Exception? MaybeException() - => httpResponse.IsSuccessStatusCode - ? null -#if NETSTANDARD - : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}"); -#else - : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); -#endif - Task ReadResponse() => httpResponse.ReadResponse(cancellationToken); async Task ReadAndConvertResponse() { diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 7a353e550..0a57d189f 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -50,8 +50,7 @@ async Task ExecuteInternal(RestRequest request, CancellationTo using var requestContent = new RequestContent(this, request); - if (Authenticator != null) - await Authenticator.Authenticate(this, request).ConfigureAwait(false); + if (Authenticator != null) await Authenticator.Authenticate(this, request).ConfigureAwait(false); var httpMethod = AsHttpMethod(request.Method); var url = BuildUri(request); @@ -61,7 +60,8 @@ async Task ExecuteInternal(RestRequest request, CancellationTo using var timeoutCts = new CancellationTokenSource(request.Timeout > 0 ? request.Timeout : int.MaxValue); using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); - var ct = cts.Token; + + var ct = cts.Token; try { var headers = new RequestHeaders() @@ -70,13 +70,11 @@ async Task ExecuteInternal(RestRequest request, CancellationTo .AddAcceptHeader(AcceptedContentTypes); message.AddHeaders(headers); - if (request.OnBeforeRequest != null) - await request.OnBeforeRequest(message).ConfigureAwait(false); + if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false); var responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false); - if (request.OnAfterRequest != null) - await request.OnAfterRequest(responseMessage).ConfigureAwait(false); + if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false); return new InternalResponse(responseMessage, url, null, timeoutCts.Token); } @@ -99,8 +97,10 @@ record InternalResponse(HttpResponseMessage? ResponseMessage, Uri Url, Exception request.CompletionOption = HttpCompletionOption.ResponseHeadersRead; var response = await ExecuteInternal(request, cancellationToken).ConfigureAwait(false); - if (response.Exception != null) { - return Options.ThrowOnAnyError ? throw response.Exception : null; + var exception = response.Exception ?? response.ResponseMessage?.MaybeException(); + + if (exception != null) { + return Options.ThrowOnAnyError ? throw exception : null; } if (response.ResponseMessage == null) return null; @@ -141,7 +141,7 @@ static HttpMethod AsHttpMethod(Method method) #if NETSTANDARD Method.Patch => new HttpMethod("PATCH"), #else - Method.Patch => HttpMethod.Patch, + Method.Patch => HttpMethod.Patch, #endif Method.Merge => new HttpMethod("MERGE"), Method.Copy => new HttpMethod("COPY"), @@ -157,11 +157,11 @@ public static RestResponse ThrowIfError(this RestResponse response) { return response; } - + public static RestResponse ThrowIfError(this RestResponse response) { var exception = response.GetException(); if (exception != null) throw exception; return response; } -} \ No newline at end of file +} diff --git a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs index fe7a2fde3..aa30a158e 100644 --- a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs +++ b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs @@ -7,7 +7,8 @@ namespace RestSharp.Tests.Integrated; public sealed class DownloadFileTests : IDisposable { public DownloadFileTests() { _server = HttpServerFixture.StartServer("Assets/Koala.jpg", FileHandler); - _client = new RestClient(_server.Url); + var options = new RestClientOptions(_server.Url) { ThrowOnAnyError = true }; + _client = new RestClient(options); } public void Dispose() => _server.Dispose(); @@ -46,6 +47,13 @@ public async Task AdvancedResponseWriter_without_ResponseWriter_reads_stream() { Assert.True(string.Compare("JFIF", tag, StringComparison.Ordinal) == 0); } + [Fact] + public async Task Handles_File_Download_Failure() { + var request = new RestRequest("Assets/Koala1.jpg"); + var task = () => _client.DownloadDataAsync(request); + await task.Should().ThrowAsync().WithMessage("Request failed with status code NotFound"); + } + [Fact] public async Task Handles_Binary_File_Download() { var request = new RestRequest("Assets/Koala.jpg"); @@ -76,4 +84,4 @@ public async Task Writes_Response_To_Stream() { Assert.Equal(expected, fromTemp); } -} \ No newline at end of file +} From 2621b17838a72e18cc4c8ee0e4d5279874054e05 Mon Sep 17 00:00:00 2001 From: Kendall Bennett Date: Wed, 9 Nov 2022 10:34:18 -0500 Subject: [PATCH 07/49] Move handling of Cookies out of HttpClient and into RestSharp, so they will not cross pollinate requests. (#1966) Make the CookieContainer a property on the request, not the client. Add tests for cookie handling. --- src/RestSharp/KnownHeaders.cs | 1 + src/RestSharp/Request/RequestHeaders.cs | 14 ++++- src/RestSharp/Request/RestRequest.cs | 16 ++++++ .../Request/RestRequestExtensions.cs | 16 ++++++ src/RestSharp/Response/RestResponse.cs | 2 +- src/RestSharp/RestClient.Async.cs | 15 ++++- src/RestSharp/RestClient.cs | 30 +++++----- src/RestSharp/RestClientExtensions.Config.cs | 18 ------ src/RestSharp/RestClientOptions.cs | 1 - .../RequestTests.cs | 57 +++++++++++++++++++ .../Server/TestServer.cs | 32 +++++++++++ 11 files changed, 165 insertions(+), 37 deletions(-) diff --git a/src/RestSharp/KnownHeaders.cs b/src/RestSharp/KnownHeaders.cs index f4c20645b..7943f9eed 100644 --- a/src/RestSharp/KnownHeaders.cs +++ b/src/RestSharp/KnownHeaders.cs @@ -30,6 +30,7 @@ public static class KnownHeaders { public const string ContentLocation = "Content-Location"; public const string ContentRange = "Content-Range"; public const string ContentType = "Content-Type"; + public const string Cookie = "Cookie"; public const string LastModified = "Last-Modified"; public const string ContentMD5 = "Content-MD5"; public const string Host = "Host"; diff --git a/src/RestSharp/Request/RequestHeaders.cs b/src/RestSharp/Request/RequestHeaders.cs index 9d53ee4d7..d15b721b4 100644 --- a/src/RestSharp/Request/RequestHeaders.cs +++ b/src/RestSharp/Request/RequestHeaders.cs @@ -14,7 +14,10 @@ // // ReSharper disable InvertIf -namespace RestSharp; + +using System.Net; + +namespace RestSharp; class RequestHeaders { public ParametersCollection Parameters { get; } = new(); @@ -33,4 +36,13 @@ public RequestHeaders AddAcceptHeader(string[] acceptedContentTypes) { return this; } + + // Add Cookie header from the cookie container + public RequestHeaders AddCookieHeaders(CookieContainer cookieContainer, Uri uri) { + var cookies = cookieContainer.GetCookieHeader(uri); + if (cookies.Length > 0) { + Parameters.AddParameter(new HeaderParameter(KnownHeaders.Cookie, cookies)); + } + return this; + } } \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index 3a13dd49c..4d81ac9ba 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Net; using RestSharp.Extensions; // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -29,6 +30,11 @@ public class RestRequest { /// public RestRequest() => Method = Method.Get; + /// + /// Constructor for a rest request to a relative resource URL and optional method + /// + /// Resource to use + /// Method to use (defaults to Method.Get> public RestRequest(string? resource, Method method = Method.Get) : this() { Resource = resource ?? ""; Method = method; @@ -58,6 +64,11 @@ static IEnumerable> ParseQuery(string query) ); } + /// + /// Constructor for a rest request to a specific resource Uri and optional method + /// + /// Resource Uri to use + /// Method to use (defaults to Method.Get> public RestRequest(Uri resource, Method method = Method.Get) : this(resource.IsAbsoluteUri ? resource.AbsoluteUri : resource.OriginalString, method) { } @@ -83,6 +94,11 @@ public RestRequest(Uri resource, Method method = Method.Get) /// public ParametersCollection Parameters { get; } = new(); + /// + /// Optional cookie container to use for the request. If not set, cookies are not passed. + /// + public CookieContainer? CookieContainer { get; set; } + /// /// Container of all the files to be uploaded with the request. /// diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index 80bfa5c13..a35da5621 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Net; using System.Text.RegularExpressions; using RestSharp.Extensions; using RestSharp.Serializers; @@ -445,6 +446,21 @@ public static RestRequest AddObject(this RestRequest request, T obj, params s return request; } + /// + /// Adds cookie to the cookie container. + /// + /// RestRequest to add the cookies to + /// Cookie name + /// Cookie value + /// Cookie path + /// Cookie domain, must not be an empty string + /// + public static RestRequest AddCookie(this RestRequest request, string name, string value, string path, string domain) { + request.CookieContainer ??= new CookieContainer(); + request.CookieContainer.Add(new Cookie(name, value, path, domain)); + return request; + } + static void CheckAndThrowsForInvalidHost(string name, string value) { static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown; diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index a9225ec23..cc7843b0b 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -64,7 +64,7 @@ internal static async Task FromHttpResponse( HttpResponseMessage httpResponse, RestRequest request, Encoding encoding, - CookieCollection cookieCollection, + CookieCollection? cookieCollection, CalculateResponseStatus calculateResponseStatus, CancellationToken cancellationToken ) { diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 0a57d189f..f317ed354 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Net; using RestSharp.Extensions; namespace RestSharp; @@ -32,7 +33,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo internalResponse.ResponseMessage!, request, Options.Encoding, - CookieContainer.GetCookies(internalResponse.Url), + request.CookieContainer!.GetCookies(internalResponse.Url), CalculateResponseStatus, cancellationToken ) @@ -64,16 +65,26 @@ async Task ExecuteInternal(RestRequest request, CancellationTo var ct = cts.Token; try { + // Make sure we have a cookie container if not provided in the request + var cookieContainer = request.CookieContainer ??= new CookieContainer(); var headers = new RequestHeaders() .AddHeaders(request.Parameters) .AddHeaders(DefaultParameters) - .AddAcceptHeader(AcceptedContentTypes); + .AddAcceptHeader(AcceptedContentTypes) + .AddCookieHeaders(cookieContainer, url); message.AddHeaders(headers); if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false); var responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false); + // Parse all the cookies from the response and update the cookie jar with cookies + if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookiesHeader)) { + foreach (var header in cookiesHeader) { + cookieContainer.SetCookies(url, header); + } + } + if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false); return new InternalResponse(responseMessage, url, null, timeoutCts.Token); diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs index b12b00d72..5abdba0d1 100644 --- a/src/RestSharp/RestClient.cs +++ b/src/RestSharp/RestClient.cs @@ -27,8 +27,6 @@ namespace RestSharp; /// Client to translate RestRequests into Http requests and process response result /// public partial class RestClient : IDisposable { - public CookieContainer CookieContainer { get; } - /// /// Content types that will be sent in the Accept header. The list is populated from the known serializers. /// If you need to send something else by default, set this property to a different value. @@ -51,7 +49,6 @@ public RestClient(RestClientOptions options, Action? configu UseDefaultSerializers(); Options = options; - CookieContainer = Options.CookieContainer ?? new CookieContainer(); _disposeHttpClient = true; var handler = new HttpClientHandler(); @@ -71,23 +68,27 @@ public RestClient() : this(new RestClientOptions()) { } /// /// - /// Sets the BaseUrl property for requests made by this client instance + /// Creates an instance of RestClient using a specific BaseUrl for requests made by this client instance /// - /// + /// Base URI for the new client public RestClient(Uri baseUrl) : this(new RestClientOptions { BaseUrl = baseUrl }) { } /// /// - /// Sets the BaseUrl property for requests made by this client instance + /// Creates an instance of RestClient using a specific BaseUrl for requests made by this client instance /// - /// + /// Base URI for this new client as a string public RestClient(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, nameof(baseUrl)))) { } + /// + /// Creates an instance of RestClient using a shared HttpClient and does not allocate one internally. + /// + /// HttpClient to use + /// True to dispose of the client, false to assume the caller does (defaults to false) public RestClient(HttpClient httpClient, bool disposeHttpClient = false) { UseDefaultSerializers(); HttpClient = httpClient; - CookieContainer = new CookieContainer(); Options = new RestClientOptions(); _disposeHttpClient = disposeHttpClient; @@ -96,15 +97,16 @@ public RestClient(HttpClient httpClient, bool disposeHttpClient = false) { } } + /// + /// Creates an instance of RestClient using a shared HttpClient and specific RestClientOptions and does not allocate one internally. + /// + /// HttpClient to use + /// RestClient options to use + /// True to dispose of the client, false to assume the caller does (defaults to false) public RestClient(HttpClient httpClient, RestClientOptions options, bool disposeHttpClient = false) { - if (options.CookieContainer != null) { - throw new ArgumentException("Custom cookie container cannot be added to the HttpClient instance", nameof(options.CookieContainer)); - } - UseDefaultSerializers(); HttpClient = httpClient; - CookieContainer = new CookieContainer(); Options = options; _disposeHttpClient = disposeHttpClient; @@ -134,9 +136,9 @@ void ConfigureHttpClient(HttpClient httpClient) { } void ConfigureHttpMessageHandler(HttpClientHandler handler) { + handler.UseCookies = false; handler.Credentials = Options.Credentials; handler.UseDefaultCredentials = Options.UseDefaultCredentials; - handler.CookieContainer = CookieContainer; handler.AutomaticDecompression = Options.AutomaticDecompression; handler.PreAuthenticate = Options.PreAuthenticate; handler.AllowAutoRedirect = Options.FollowRedirects; diff --git a/src/RestSharp/RestClientExtensions.Config.cs b/src/RestSharp/RestClientExtensions.Config.cs index 205812923..8868f7736 100644 --- a/src/RestSharp/RestClientExtensions.Config.cs +++ b/src/RestSharp/RestClientExtensions.Config.cs @@ -13,7 +13,6 @@ // limitations under the License. // -using System.Net; using System.Text; using RestSharp.Authenticators; using RestSharp.Extensions; @@ -43,23 +42,6 @@ public static partial class RestClientExtensions { public static RestClient UseQueryEncoder(this RestClient client, Func queryEncoder) => client.With(x => x.EncodeQuery = queryEncoder); - /// - /// Adds cookie to the cookie container. - /// - /// - /// Cookie name - /// Cookie value - /// Cookie path - /// Cookie domain, must not be an empty string - /// - public static RestClient AddCookie(this RestClient client, string name, string value, string path, string domain) { - lock (client.CookieContainer) { - client.CookieContainer.Add(new Cookie(name, value, path, domain)); - } - - return client; - } - public static RestClient UseAuthenticator(this RestClient client, IAuthenticator authenticator) => client.With(x => x.Authenticator = authenticator); } \ No newline at end of file diff --git a/src/RestSharp/RestClientOptions.cs b/src/RestSharp/RestClientOptions.cs index 43fec3f3a..83d820660 100644 --- a/src/RestSharp/RestClientOptions.cs +++ b/src/RestSharp/RestClientOptions.cs @@ -74,7 +74,6 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba public CacheControlHeaderValue? CachePolicy { get; set; } public bool FollowRedirects { get; set; } = true; public bool? Expect100Continue { get; set; } = null; - public CookieContainer? CookieContainer { get; set; } public string? UserAgent { get; set; } = DefaultUserAgent; /// diff --git a/test/RestSharp.Tests.Integrated/RequestTests.cs b/test/RestSharp.Tests.Integrated/RequestTests.cs index 9eac238a0..b1fd86041 100644 --- a/test/RestSharp.Tests.Integrated/RequestTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestTests.cs @@ -51,6 +51,63 @@ public async Task Can_Perform_GET_Async() { response.Content.Should().Be(val); } + [Fact] + public async Task Can_Perform_GET_Async_With_Request_Cookies() { + var request = new RestRequest("get-cookies") { + CookieContainer = new CookieContainer() + }; + request.CookieContainer.Add(new Cookie("cookie", "value", null, _client.Options.BaseUrl.Host)); + request.CookieContainer.Add(new Cookie("cookie2", "value2", null, _client.Options.BaseUrl.Host)); + var response = await _client.ExecuteAsync(request); + response.Content.Should().Be("[\"cookie=value\",\"cookie2=value2\"]"); + } + + [Fact] + public async Task Can_Perform_GET_Async_With_Response_Cookies() { + var request = new RestRequest("set-cookies"); + var response = await _client.ExecuteAsync(request); + response.Content.Should().Be("success"); + + // Check we got all our cookies + var domain = _client.Options.BaseUrl.Host; + var cookie = response.Cookies!.First(p => p.Name == "cookie1"); + Assert.Equal("value1", cookie.Value); + Assert.Equal("/", cookie.Path); + Assert.Equal(domain, cookie.Domain); + Assert.Equal(DateTime.MinValue, cookie.Expires); + Assert.False(cookie.HttpOnly); + + // Cookie 2 should vanish as the path will not match + cookie = response.Cookies!.FirstOrDefault(p => p.Name == "cookie2"); + Assert.Null(cookie); + + // Check cookie3 has a valid expiration + cookie = response.Cookies!.First(p => p.Name == "cookie3"); + Assert.Equal("value3", cookie.Value); + Assert.Equal("/", cookie.Path); + Assert.Equal(domain, cookie.Domain); + Assert.True(cookie.Expires > DateTime.Now); + + // Check cookie4 has a valid expiration + cookie = response.Cookies!.First(p => p.Name == "cookie4"); + Assert.Equal("value4", cookie.Value); + Assert.Equal("/", cookie.Path); + Assert.Equal(domain, cookie.Domain); + Assert.True(cookie.Expires > DateTime.Now); + + // Cookie 5 should vanish as the request is not SSL + cookie = response.Cookies!.FirstOrDefault(p => p.Name == "cookie5"); + Assert.Null(cookie); + + // Check cookie6 should be http only + cookie = response.Cookies!.First(p => p.Name == "cookie6"); + Assert.Equal("value6", cookie.Value); + Assert.Equal("/", cookie.Path); + Assert.Equal(domain, cookie.Domain); + Assert.Equal(DateTime.MinValue, cookie.Expires); + Assert.True(cookie.HttpOnly); + } + [Fact] public async Task Can_Timeout_GET_Async() { var request = new RestRequest("timeout").AddBody("Body_Content"); diff --git a/test/RestSharp.Tests.Integrated/Server/TestServer.cs b/test/RestSharp.Tests.Integrated/Server/TestServer.cs index fa7a365da..7570bdd66 100644 --- a/test/RestSharp.Tests.Integrated/Server/TestServer.cs +++ b/test/RestSharp.Tests.Integrated/Server/TestServer.cs @@ -37,6 +37,10 @@ public HttpServer(ITestOutputHelper output = null) { _app.MapGet("request-echo", async context => await context.Request.BodyReader.AsStream().CopyToAsync(context.Response.BodyWriter.AsStream())); _app.MapDelete("delete", () => new TestResponse { Message = "Works!" }); + // Cookies + _app.MapGet("get-cookies", HandleCookies); + _app.MapGet("set-cookies", HandleSetCookies); + // PUT _app.MapPut( ContentResource, @@ -60,6 +64,34 @@ IResult HandleHeaders(HttpContext ctx) { return Results.Ok(response); } + IResult HandleCookies(HttpContext ctx) { + var results = new List(); + foreach (var (key, value) in ctx.Request.Cookies) { + results.Add($"{key}={value}"); + } + return Results.Ok(results); + } + + IResult HandleSetCookies(HttpContext ctx) { + ctx.Response.Cookies.Append("cookie1", "value1"); + ctx.Response.Cookies.Append("cookie2", "value2", new CookieOptions { + Path = "/path_extra" + }); + ctx.Response.Cookies.Append("cookie3", "value3", new CookieOptions { + Expires = DateTimeOffset.Now.AddDays(2) + }); + ctx.Response.Cookies.Append("cookie4", "value4", new CookieOptions { + MaxAge = TimeSpan.FromSeconds(100) + }); + ctx.Response.Cookies.Append("cookie5", "value5", new CookieOptions { + Secure = true + }); + ctx.Response.Cookies.Append("cookie6", "value6", new CookieOptions { + HttpOnly = true + }); + return Results.Content("success"); + } + async Task HandleUpload(HttpRequest req) { if (!req.HasFormContentType) { return Results.BadRequest("It's not a form"); From a63d57fe8d04de5cecbd1dd321ca4f77c675cafc Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Fri, 11 Nov 2022 11:13:49 +0100 Subject: [PATCH 08/49] Merge with dev --- src/Directory.Build.props | 1 + src/RestSharp/Extensions/HttpResponseExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7b6217a89..eb85a8c2b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,6 +15,7 @@ snupkg true $(NoWarn);1591 + 11 diff --git a/src/RestSharp/Extensions/HttpResponseExtensions.cs b/src/RestSharp/Extensions/HttpResponseExtensions.cs index 53b873e6c..4558f2d7a 100644 --- a/src/RestSharp/Extensions/HttpResponseExtensions.cs +++ b/src/RestSharp/Extensions/HttpResponseExtensions.cs @@ -19,7 +19,7 @@ public static class HttpResponseExtensions { internal static Exception? MaybeException(this HttpResponseMessage httpResponse) => httpResponse.IsSuccessStatusCode ? null -#if NETSTANDARD +#if NETSTANDARD || NETFRAMEWORK : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}"); #else : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); From 1c34f278fe7079598d7ec9756244fa8163620339 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Fri, 11 Nov 2022 11:20:54 +0100 Subject: [PATCH 09/49] Add AWS support --- README.md | 27 +++++---------------------- docs/.vuepress/public/aws_logo.png | Bin 0 -> 28712 bytes 2 files changed, 5 insertions(+), 22 deletions(-) create mode 100644 docs/.vuepress/public/aws_logo.png diff --git a/README.md b/README.md index b79affa84..063e4e89b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ What RestSharp adds to `HttpClient`: - Multiple ways to add a request body, including JSON, XML, and form data - Built-in serialization and deserilization of JSON and XML -## RestSharp vNext +**RestSharp is supported by [AWS](https://aws.amazon.com/developer/language/net/solutions/).** + +## RestSharp vNext (v107+) Finally, RestSharp has moved to `HttpClient`. We also deprecated the following: - SimpleJson in favour of `System.Text.Json.JsonSerialzer` @@ -66,7 +68,7 @@ Find RestSharp on Twitter: [@RestSharp][2] ### .NET Foundation -This project is supported by the [.NET Foundation](https://dotnetfoundation.org). +This project is a part of the [.NET Foundation](https://dotnetfoundation.org). ### Code Contributors @@ -75,26 +77,7 @@ This project exists thanks to all the people who contribute. ### Financial Contributors -Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/RestSharp/contribute)] - -#### Individuals - - - -#### Organizations - -Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/RestSharp/contribute)] - - - - - - - - - - - +Become a financial contributor and help us sustain our community. [Contribute](https://github.com/sponsors/restsharp) ### License: Apache License 2.0 diff --git a/docs/.vuepress/public/aws_logo.png b/docs/.vuepress/public/aws_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..68304e083e249eb4d4c05ee11b8b064763f0c25d GIT binary patch literal 28712 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