From 25c41240d3a6c05acf8200e9d0708637114c18e0 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Mon, 27 May 2024 11:58:43 +0200 Subject: [PATCH 1/6] Force not escape query --- src/RestSharp/Request/UriExtensions.cs | 20 ++++++++++++++------ test/RestSharp.Tests/UrlBuilderTests.cs | 12 ++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs index 95ad66611..c1c1f9b77 100644 --- a/src/RestSharp/Request/UriExtensions.cs +++ b/src/RestSharp/Request/UriExtensions.cs @@ -29,7 +29,7 @@ public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { : throw new ArgumentException("Both BaseUrl and Resource are empty", nameof(resource)); } - var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri(baseUrl.AbsoluteUri + "/"); + var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri($"{baseUrl.AbsoluteUri}/"); return assembled != null ? new Uri(usingBaseUri, assembled) : baseUrl; } @@ -37,10 +37,18 @@ public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { public static Uri AddQueryString(this Uri uri, string? query) { if (query == null) return uri; - var absoluteUri = uri.AbsoluteUri; - var separator = absoluteUri.Contains('?') ? "&" : "?"; - - return new Uri($"{absoluteUri}{separator}{query}"); + var absoluteUri = uri.AbsoluteUri; + var separator = absoluteUri.Contains('?') ? "&" : "?"; + + var result = +#if NET6_0_OR_GREATER + new Uri($"{absoluteUri}{separator}{query}", new UriCreationOptions{DangerousDisablePathAndQueryCanonicalization = true}); +#else +#pragma warning disable CS0618 // Type or member is obsolete + new Uri($"{absoluteUri}{separator}{query}", false); +#pragma warning restore CS0618 // Type or member is obsolete +#endif + return result; } public static UrlSegmentParamsValues GetUrlSegmentParamsValues( @@ -72,4 +80,4 @@ params ParametersCollection[] parametersCollections } } -record UrlSegmentParamsValues(Uri Uri, string Resource); +record UrlSegmentParamsValues(Uri Uri, string Resource); \ No newline at end of file diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs index 0373919a5..631be972b 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -361,4 +361,16 @@ public void Should_use_ipv6_address() { actual.HostNameType.Should().Be(UriHostNameType.IPv6); actual.AbsoluteUri.Should().Be("https://[fe80::290:e8ff:fe8b:2537]:8443/api/v1/auth"); } + + [Fact] + public async Task Should_not_encode_pipe() { + var client = new RestClient("https://enggmsn5amo79.x.pipedream.net"); + var request = new RestRequest("resource"); + request.AddQueryParameter("ids", "in:001|116", true); + + // var actual = client.BuildUri(request); + // var expected = new Uri("http://example.com/resource?ids=in:001|116"); + // actual.Should().Be(expected); + await client.GetAsync(request); + } } \ No newline at end of file From 27bec285727c4c13730d39608a46fa9a1e4d0abe Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Mon, 27 May 2024 12:02:28 +0200 Subject: [PATCH 2/6] Fix the test --- src/RestSharp/BuildUriExtensions.cs | 2 +- test/RestSharp.Tests/UrlBuilderTests.cs | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/RestSharp/BuildUriExtensions.cs b/src/RestSharp/BuildUriExtensions.cs index 13c663b81..ba6676230 100644 --- a/src/RestSharp/BuildUriExtensions.cs +++ b/src/RestSharp/BuildUriExtensions.cs @@ -84,7 +84,7 @@ string EncodeParameter(Parameter parameter) } static void DoBuildUriValidations(IRestClient client, RestRequest request) { - if (client.Options.BaseUrl == null && !request.Resource.ToLowerInvariant().StartsWith("http")) + if (client.Options.BaseUrl == null && !request.Resource.StartsWith("http", StringComparison.InvariantCultureIgnoreCase)) throw new ArgumentOutOfRangeException( nameof(request), "Request resource doesn't contain a valid scheme for an empty base URL of the client" diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs index 631be972b..73afa056b 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -363,14 +363,13 @@ public void Should_use_ipv6_address() { } [Fact] - public async Task Should_not_encode_pipe() { - var client = new RestClient("https://enggmsn5amo79.x.pipedream.net"); + public void Should_not_encode_pipe() { + var client = new RestClient("http://example.com/"); var request = new RestRequest("resource"); - request.AddQueryParameter("ids", "in:001|116", true); + request.AddQueryParameter("ids", "in:001|116", false); - // var actual = client.BuildUri(request); - // var expected = new Uri("http://example.com/resource?ids=in:001|116"); - // actual.Should().Be(expected); - await client.GetAsync(request); + var actual = client.BuildUri(request); + var expected = "http://example.com/resource?ids=in:001|116"; + actual.ToString().Should().Be(expected); } } \ No newline at end of file From af14a202a7a334a5e7122870da3f6635e36b0e75 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Mon, 27 May 2024 12:42:49 +0200 Subject: [PATCH 3/6] Fix the URL builder tests --- test/RestSharp.Tests/UrlBuilderTests.cs | 274 +++++++----------------- 1 file changed, 79 insertions(+), 195 deletions(-) diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs index 73afa056b..8db92717e 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -10,59 +10,31 @@ public class UrlBuilderTests { public void GET_with_empty_base_and_query_parameters_without_encoding() { var request = new RestRequest("http://example.com/resource?param1=value1") .AddQueryParameter("foo", "bar,baz", false); - var expected = new Uri("http://example.com/resource?param1=value1&foo=bar,baz"); - - using var client = new RestClient(); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri(request, "http://example.com/resource?param1=value1&foo=bar,baz"); } [Fact] public void GET_with_empty_base_and_resource_containing_tokens() { - var request = new RestRequest("http://example.com/resource/{foo}"); - request.AddUrlSegment("foo", "bar"); - - using var client = new RestClient(); - - var expected = new Uri("http://example.com/resource/bar"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); + var request = new RestRequest("http://example.com/resource/{foo}").AddUrlSegment("foo", "bar"); + AssertUri(request, "http://example.com/resource/bar"); } [Fact] public void GET_with_empty_request() { var request = new RestRequest(); - var expected = new Uri("http://example.com/"); - - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/"); } [Fact] public void GET_with_empty_request_and_bare_hostname() { var request = new RestRequest(); - var expected = new Uri("http://example.com/"); - - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/"); } [Fact] public void GET_with_empty_request_and_query_parameters_without_encoding() { - var request = new RestRequest(); - request.AddQueryParameter("foo", "bar,baz", false); - var expected = new Uri("http://example.com/resource?param1=value1&foo=bar,baz"); - - using var client = new RestClient("http://example.com/resource?param1=value1"); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest().AddQueryParameter("foo", "bar,baz", false); + AssertUri("http://example.com/resource?param1=value1", request, "http://example.com/resource?param1=value1&foo=bar,baz"); } [Fact] @@ -75,188 +47,115 @@ public void GET_with_Invalid_Url_string_throws_exception() [Fact] public void GET_with_leading_slash() { - var request = new RestRequest("/resource"); - var expected = new Uri("http://example.com/resource"); - - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest("/resource"); + AssertUri("http://example.com", request, "http://example.com/resource"); } [Fact] public void GET_with_leading_slash_and_baseurl_trailing_slash() { - var request = new RestRequest("/resource"); - request.AddParameter("foo", "bar"); - var expected = new Uri("http://example.com/resource?foo=bar"); - - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest("/resource").AddParameter("foo", "bar"); + AssertUri("http://example.com", request, "http://example.com/resource?foo=bar"); } [Fact] public void GET_with_multiple_instances_of_same_key() { - var request = new RestRequest("v1/people/~/network/updates"); - request.AddParameter("type", "STAT"); - request.AddParameter("type", "PICT"); - request.AddParameter("count", "50"); - request.AddParameter("start", "50"); - var expected = new Uri("https://api.linkedin.com/v1/people/~/network/updates?type=STAT&type=PICT&count=50&start=50"); - - using var client = new RestClient("https://api.linkedin.com"); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest("v1/people/~/network/updates") + .AddParameter("type", "STAT") + .AddParameter("type", "PICT") + .AddParameter("count", "50") + .AddParameter("start", "50"); + AssertUri("https://api.linkedin.com", request, "https://api.linkedin.com/v1/people/~/network/updates?type=STAT&type=PICT&count=50&start=50"); } [Fact] public void GET_with_resource_containing_null_token() { var request = new RestRequest("/resource/{foo}"); - Assert.Throws(() => request.AddUrlSegment("foo", null)); + Assert.Throws(() => request.AddUrlSegment("foo", null!)); } [Fact] public void GET_with_resource_containing_slashes() { - var request = new RestRequest("resource/foo"); - var expected = new Uri("http://example.com/resource/foo"); - - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest("resource/foo"); + AssertUri("http://example.com", request, "http://example.com/resource/foo"); } [Fact] public void GET_with_resource_containing_tokens() { - var request = new RestRequest("resource/{foo}"); - request.AddUrlSegment("foo", "bar"); - var expected = new Uri("http://example.com/resource/bar"); - - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest("resource/{foo}").AddUrlSegment("foo", "bar"); + AssertUri("http://example.com", request, "http://example.com/resource/bar"); } [Fact] public void GET_with_Uri_and_resource_containing_tokens() { - var request = new RestRequest("resource/{baz}"); - request.AddUrlSegment("foo", "bar"); - request.AddUrlSegment("baz", "bat"); - var expected = new Uri("http://example.com/bar/resource/bat"); - - using var client = new RestClient(new Uri("http://example.com/{foo}")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest("resource/{baz}") + .AddUrlSegment("foo", "bar") + .AddUrlSegment("baz", "bat"); + AssertUri("http://example.com/{foo}", request, "http://example.com/bar/resource/bat"); } [Fact] public void GET_with_Uri_containing_tokens() { - var request = new RestRequest(); - request.AddUrlSegment("foo", "bar"); - var expected = new Uri("http://example.com/bar"); - - using var client = new RestClient(new Uri("http://example.com/{foo}")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + var request = new RestRequest().AddUrlSegment("foo", "bar"); + AssertUri("http://example.com/{foo}", request, "http://example.com/bar"); } [Fact] public void GET_with_Url_string_and_resource_containing_tokens() { - var request = new RestRequest("resource/{baz}"); - request.AddUrlSegment("foo", "bar"); - request.AddUrlSegment("baz", "bat"); - var expected = new Uri("http://example.com/bar/resource/bat"); + var request = new RestRequest("resource/{baz}") + .AddUrlSegment("foo", "bar") + .AddUrlSegment("baz", "bat"); - using var client = new RestClient("http://example.com/{foo}"); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com/{foo}", request, "http://example.com/bar/resource/bat"); } [Fact] public void GET_with_Url_string_containing_tokens() { - var request = new RestRequest(); - request.AddUrlSegment("foo", "bar"); - var expected = new Uri("http://example.com/bar"); - - using var client = new RestClient("http://example.com/{foo}"); + var request = new RestRequest().AddUrlSegment("foo", "bar"); - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com/{foo}", request, "http://example.com/bar"); } [Fact] public void GET_wth_trailing_slash_and_query_parameters() { - var request = new RestRequest("/resource/"); - request.AddParameter("foo", "bar"); - var expected = new Uri("http://example.com/resource/?foo=bar"); + var request = new RestRequest("/resource/").AddParameter("foo", "bar"); - using var client = new RestClient("http://example.com"); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/resource/?foo=bar"); } [Fact] public void POST_with_leading_slash() { - var request = new RestRequest("/resource", Method.Post); - var expected = new Uri("http://example.com/resource"); - - using var client = new RestClient(new Uri("http://example.com")); + var request = new RestRequest("/resource", Method.Post); - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/resource"); } [Fact] public void POST_with_leading_slash_and_baseurl_trailing_slash() { - var request = new RestRequest("/resource", Method.Post); - var expected = new Uri("http://example.com/resource"); + var request = new RestRequest("/resource", Method.Post); - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/resource"); } [Fact] public void POST_with_querystring_containing_tokens() { - var request = new RestRequest("resource", Method.Post); - request.AddParameter("foo", "bar", ParameterType.QueryString); - var expected = new Uri("http://example.com/resource?foo=bar"); - - using var client = new RestClient("http://example.com"); + var request = new RestRequest("resource", Method.Post).AddParameter("foo", "bar", ParameterType.QueryString); - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/resource?foo=bar"); } [Fact] public void POST_with_resource_containing_slashes() { - var request = new RestRequest("resource/foo", Method.Post); - var expected = new Uri("http://example.com/resource/foo"); + var request = new RestRequest("resource/foo", Method.Post); - using var client = new RestClient(new Uri("http://example.com")); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/resource/foo"); } [Fact] public void POST_with_resource_containing_tokens() { var request = new RestRequest("resource/{foo}", Method.Post); request.AddUrlSegment("foo", "bar"); - var expected = new Uri("http://example.com/resource/bar"); - - using var client = new RestClient(new Uri("http://example.com")); - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com", request, "http://example.com/resource/bar"); } [Fact] @@ -264,64 +163,46 @@ public void Should_add_parameter_if_it_is_new() { var request = new RestRequest(); request.AddOrUpdateParameter("param2", "value2"); request.AddOrUpdateParameter("param3", "value3"); - var expected = new Uri("http://example.com/resource?param1=value1¶m2=value2¶m3=value3"); - using var client = new RestClient("http://example.com/resource?param1=value1"); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com/resource?param1=value1", request, "http://example.com/resource?param1=value1¶m2=value2¶m3=value3"); } [Fact] public void Should_build_uri_using_selected_encoding() { - var request = new RestRequest(); // adding parameter with o-slash character which is encoded differently between // utf-8 and iso-8859-1 - request.AddOrUpdateParameter("town", "Hillerød"); - var expectedDefaultEncoding = new Uri("http://example.com/resource?town=Hiller%C3%B8d"); - var expectedIso89591Encoding = new Uri("http://example.com/resource?town=Hiller%f8d"); + var request = new RestRequest().AddOrUpdateParameter("town", "Hillerød"); - using var client1 = new RestClient(new RestClientOptions("http://example.com/resource")); - Assert.Equal(expectedDefaultEncoding, client1.BuildUri(request)); + const string expectedDefaultEncoding = "http://example.com/resource?town=Hiller%c3%b8d"; + const string expectedIso89591Encoding = "http://example.com/resource?town=Hiller%f8d"; + + AssertUri("http://example.com/resource", request, expectedDefaultEncoding); using var client2 = new RestClient(new RestClientOptions("http://example.com/resource") { Encoding = Encoding.GetEncoding("ISO-8859-1") }); - Assert.Equal(expectedIso89591Encoding, client2.BuildUri(request)); + AssertUri(client2, request, expectedIso89591Encoding); } [Fact] public void Should_build_uri_with_resource_full_uri() { - var request = new RestRequest("https://www.example1.com/connect/authorize"); - var expected = new Uri("https://www.example1.com/connect/authorize"); - - using var client = new RestClient("https://www.example1.com/"); + var request = new RestRequest("https://www.example1.com/connect/authorize"); - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("https://www.example1.com/", request, "https://www.example1.com/connect/authorize"); } [Fact] public void Should_encode_colon() { - var request = new RestRequest(); // adding parameter with o-slash character which is encoded differently between // utf-8 and iso-8859-1 - request.AddOrUpdateParameter("parameter", "some:value"); - - using var client = new RestClient("http://example.com/resource"); + var request = new RestRequest().AddOrUpdateParameter("parameter", "some:value"); - var expectedDefaultEncoding = new Uri("http://example.com/resource?parameter=some%3avalue"); - Assert.Equal(expectedDefaultEncoding, client.BuildUri(request)); + AssertUri("http://example.com/resource", request, "http://example.com/resource?parameter=some%3avalue"); } [Fact] public void Should_not_duplicate_question_mark() { - var request = new RestRequest(); - request.AddParameter("param2", "value2"); - var expected = new Uri("http://example.com/resource?param1=value1¶m2=value2"); - - using var client = new RestClient("http://example.com/resource?param1=value1"); + var request = new RestRequest().AddParameter("param2", "value2"); - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com/resource?param1=value1", request, "http://example.com/resource?param1=value1¶m2=value2"); } [Fact] @@ -331,23 +212,16 @@ public void Should_not_touch_request_url() { var req = new RestRequest(requestUrl, Method.Post); - using var client = new RestClient(baseUrl); - - var resultUrl = client.BuildUri(req).ToString(); - resultUrl.Should().Be($"{baseUrl}/{requestUrl}"); + AssertUri(baseUrl, req, $"{baseUrl}/{requestUrl}"); } [Fact] public void Should_update_parameter_if_it_already_exists() { - var request = new RestRequest(); - request.AddOrUpdateParameter("param2", "value2"); - request.AddOrUpdateParameter("param2", "value2-1"); - var expected = new Uri("http://example.com/resource?param1=value1¶m2=value2-1"); + var request = new RestRequest() + .AddOrUpdateParameter("param2", "value2") + .AddOrUpdateParameter("param2", "value2-1"); - using var client = new RestClient("http://example.com/resource?param1=value1"); - - var output = client.BuildUri(request); - Assert.Equal(expected, output); + AssertUri("http://example.com/resource?param1=value1", request, "http://example.com/resource?param1=value1¶m2=value2-1"); } [Fact] @@ -364,12 +238,22 @@ public void Should_use_ipv6_address() { [Fact] public void Should_not_encode_pipe() { - var client = new RestClient("http://example.com/"); - var request = new RestRequest("resource"); - request.AddQueryParameter("ids", "in:001|116", false); - + var request = new RestRequest("resource").AddQueryParameter("ids", "in:001|116", false); + AssertUri("http://example.com/", request, "http://example.com/resource?ids=in:001|116"); + } + + static void AssertUri(RestClient client, RestRequest request, string expected) { var actual = client.BuildUri(request); - var expected = "http://example.com/resource?ids=in:001|116"; - actual.ToString().Should().Be(expected); + actual.AbsoluteUri.Should().Be(expected); + } + + static void AssertUri(string basePath, RestRequest request, string expected) { + using var client = new RestClient(basePath); + AssertUri(client, request, expected); + } + + static void AssertUri(RestRequest request, string expected) { + using var client = new RestClient(); + AssertUri(client, request, expected); } } \ No newline at end of file From 41a2ba42332bb1b3530791e14d6468f529876a8b Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Mon, 27 May 2024 12:56:25 +0200 Subject: [PATCH 4/6] Encode the resource query as it should be. --- src/RestSharp/Request/RestRequest.cs | 18 +++++++++--------- test/RestSharp.Tests/UrlBuilderTests.cs | 10 ++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index 40b8e074c..bdb725b70 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net; using System.Net.Http.Headers; -using RestSharp.Authenticators; -using RestSharp.Extensions; -using RestSharp.Interceptors; // ReSharper disable ReplaceSubstringWithRangeIndexer // ReSharper disable UnusedAutoPropertyAccessor.Global namespace RestSharp; +using Authenticators; +using Extensions; +using Interceptors; + /// /// Container for data used to make requests /// @@ -53,7 +53,7 @@ public RestRequest(string? resource, Method method = Method.Get) : this() { var queryParams = ParseQuery(Resource.Substring(queryStringStart + 1)); Resource = Resource.Substring(0, queryStringStart); - foreach (var param in queryParams) this.AddQueryParameter(param.Key, param.Value, false); + foreach (var param in queryParams) this.AddQueryParameter(param.Key, param.Value); return; @@ -84,12 +84,12 @@ public RestRequest(Uri resource, Method method = Method.Get) /// Always send a multipart/form-data request - even when no Files are present. /// public bool AlwaysMultipartFormData { get; set; } - + /// /// Always send a file as request content without multipart/form-data request - even when the request contains only one file parameter /// public bool AlwaysSingleFileAsContent { get; set; } - + /// /// When set to true, parameter values in a multipart form data requests will be enclosed in /// quotation marks. Default is false. Enable it if the remote endpoint requires parameters @@ -232,7 +232,7 @@ public Func? AdvancedResponseWri _advancedResponseHandler = value; } } - + /// /// Request-level interceptors. Will be combined with client-level interceptors if set. /// @@ -255,4 +255,4 @@ public RestRequest RemoveParameter(Parameter parameter) { } internal RestRequest AddFile(FileParameter file) => this.With(x => x._files.Add(file)); -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs index 8db92717e..df937cddc 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -236,6 +236,16 @@ public void Should_use_ipv6_address() { actual.AbsoluteUri.Should().Be("https://[fe80::290:e8ff:fe8b:2537]:8443/api/v1/auth"); } + [Fact] + public void Should_encode_resource() { + const string baseUrl = "https://example.com"; + const string resource = "resource?param=value with spaces"; + + var request = new RestRequest(resource); + var uri = new Uri($"{baseUrl}/{resource}"); + AssertUri(baseUrl, request, uri.AbsoluteUri); + } + [Fact] public void Should_not_encode_pipe() { var request = new RestRequest("resource").AddQueryParameter("ids", "in:001|116", false); From af7d5727fb3bd4269fdb16479f926cda2dbe518c Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Mon, 27 May 2024 15:46:58 +0200 Subject: [PATCH 5/6] Some more tweaks for query encoding but it still doesn't seem to work on .NET 4.8 --- src/RestSharp/Request/Parsers.cs | 60 +++++++++++++++++++ src/RestSharp/Request/RestRequest.cs | 35 +++++------ src/RestSharp/Request/UriExtensions.cs | 34 +++++++++-- .../ResourceStringParametersTests.cs | 2 + test/RestSharp.Tests/ParametersTests.cs | 2 +- test/RestSharp.Tests/RestClientTests.cs | 32 ---------- test/RestSharp.Tests/RestRequestTests.cs | 17 ++++-- test/RestSharp.Tests/UrlBuilderTests.cs | 38 ++++++++++-- 8 files changed, 154 insertions(+), 66 deletions(-) create mode 100644 src/RestSharp/Request/Parsers.cs diff --git a/src/RestSharp/Request/Parsers.cs b/src/RestSharp/Request/Parsers.cs new file mode 100644 index 000000000..bcf01b276 --- /dev/null +++ b/src/RestSharp/Request/Parsers.cs @@ -0,0 +1,60 @@ +// 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; +using System.Web; + +namespace RestSharp; + +static class Parsers { + // ReSharper disable once CognitiveComplexity + public static IEnumerable> ParseQueryString(string query, Encoding encoding) { + Ensure.NotNull(query, nameof(query)); + Ensure.NotNull(encoding, nameof(encoding)); + var length = query.Length; + var startIndex1 = query[0] == '?' ? 1 : 0; + + if (length == startIndex1) + yield break; + + while (startIndex1 <= length) { + var startIndex2 = -1; + var num = -1; + + for (var index = startIndex1; index < length; ++index) { + if (startIndex2 == -1 && query[index] == '=') + startIndex2 = index + 1; + else if (query[index] == '&') { + num = index; + break; + } + } + + string? name; + + if (startIndex2 == -1) { + name = null; + startIndex2 = startIndex1; + } + else + name = HttpUtility.UrlDecode(query.Substring(startIndex1, startIndex2 - startIndex1 - 1), encoding); + + if (num < 0) + num = query.Length; + startIndex1 = num + 1; + var str = HttpUtility.UrlDecode(query.Substring(startIndex2, num - startIndex2), encoding); + yield return new KeyValuePair(name ?? "", str); + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index bdb725b70..8dd23e550 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -13,6 +13,8 @@ // limitations under the License. using System.Net.Http.Headers; +using System.Text; +using System.Web; // ReSharper disable ReplaceSubstringWithRangeIndexer // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -39,35 +41,30 @@ public class RestRequest { /// Constructor for a rest request to a relative resource URL and optional method /// /// Resource to use - /// Method to use (defaults to Method.Get> + /// Method to use. Default is Method.Get. public RestRequest(string? resource, Method method = Method.Get) : this() { Resource = resource ?? ""; - Method = method; + Method = method; - if (string.IsNullOrWhiteSpace(resource)) return; + if (string.IsNullOrWhiteSpace(resource)) { + Resource = ""; + return; + } var queryStringStart = Resource.IndexOf('?'); - if (queryStringStart < 0 || Resource.IndexOf('=') <= queryStringStart) return; + if (queryStringStart < 0 || Resource.IndexOf('=') <= queryStringStart) { + return; + } - var queryParams = ParseQuery(Resource.Substring(queryStringStart + 1)); + var queryString = Resource.Substring(queryStringStart + 1); Resource = Resource.Substring(0, queryStringStart); - foreach (var param in queryParams) this.AddQueryParameter(param.Key, param.Value); - - return; + var queryParameters = Parsers.ParseQueryString(queryString, Encoding.UTF8); - static IEnumerable> ParseQuery(string query) - => query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) - .Select( - x => { - var position = x.IndexOf('='); - - return position > 0 - ? new KeyValuePair(x.Substring(0, position), x.Substring(position + 1)) - : new KeyValuePair(x, null); - } - ); + foreach (var parameter in queryParameters) { + this.AddQueryParameter(parameter.Key, parameter.Value); + } } /// diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs index c1c1f9b77..41485467b 100644 --- a/src/RestSharp/Request/UriExtensions.cs +++ b/src/RestSharp/Request/UriExtensions.cs @@ -18,20 +18,44 @@ namespace RestSharp; static class UriExtensions { +#if NET6_0_OR_GREATER + internal static UriCreationOptions UriOptions = new() { DangerousDisablePathAndQueryCanonicalization = true }; +#endif + public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { var assembled = resource; +#if NET6_0_OR_GREATER + if (assembled.IsNotEmpty() && assembled.StartsWith('/')) assembled = assembled[1..]; +#else if (assembled.IsNotEmpty() && assembled.StartsWith("/")) assembled = assembled.Substring(1); +#endif if (baseUrl == null || baseUrl.AbsoluteUri.IsEmpty()) { return assembled.IsNotEmpty() - ? new Uri(assembled) +#if NET6_0_OR_GREATER + ? new Uri(assembled, UriOptions) +#else + ? new Uri(assembled, false) +#endif : throw new ArgumentException("Both BaseUrl and Resource are empty", nameof(resource)); } - var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri($"{baseUrl.AbsoluteUri}/"); +#if NET6_0_OR_GREATER + var usingBaseUri = baseUrl.AbsoluteUri.EndsWith('/') || assembled.IsEmpty() ? baseUrl : new Uri($"{baseUrl.AbsoluteUri}/", UriOptions); - return assembled != null ? new Uri(usingBaseUri, assembled) : baseUrl; + var isResourceAbsolute = false; + // ReSharper disable once InvertIf + if (assembled != null) { + var resourceUri = new Uri(assembled, UriKind.RelativeOrAbsolute); + isResourceAbsolute = resourceUri.IsAbsoluteUri; + } + + return assembled != null ? new Uri(isResourceAbsolute ? assembled : $"{usingBaseUri.AbsoluteUri}{assembled}", UriOptions) : baseUrl; +#else + var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri($"{baseUrl.AbsoluteUri}/", false); + return assembled != null ? new Uri(usingBaseUri, assembled, false) : baseUrl; +#endif } public static Uri AddQueryString(this Uri uri, string? query) { @@ -40,9 +64,9 @@ public static Uri AddQueryString(this Uri uri, string? query) { var absoluteUri = uri.AbsoluteUri; var separator = absoluteUri.Contains('?') ? "&" : "?"; - var result = + var result = #if NET6_0_OR_GREATER - new Uri($"{absoluteUri}{separator}{query}", new UriCreationOptions{DangerousDisablePathAndQueryCanonicalization = true}); + new Uri($"{absoluteUri}{separator}{query}", new UriCreationOptions { DangerousDisablePathAndQueryCanonicalization = true }); #else #pragma warning disable CS0618 // Type or member is obsolete new Uri($"{absoluteUri}{separator}{query}", false); diff --git a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs index e046cf926..f4001c95c 100644 --- a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs +++ b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs @@ -20,6 +20,8 @@ public async Task Should_keep_to_parameters_with_the_same_name() { using var client = new RestClient(_server.Url!); var request = new RestRequest(parameters); + var uri = client.BuildUri(request); + await client.GetAsync(request); var query = new Uri(url).Query; diff --git a/test/RestSharp.Tests/ParametersTests.cs b/test/RestSharp.Tests/ParametersTests.cs index e9c702fbc..e8ed219c5 100644 --- a/test/RestSharp.Tests/ParametersTests.cs +++ b/test/RestSharp.Tests/ParametersTests.cs @@ -44,7 +44,7 @@ public void AddUrlSegmentModifiesUrlSegmentWithInt() { using var client = new RestClient(BaseUrl); var actual = client.BuildUri(request).AbsolutePath; - expected.Should().BeEquivalentTo(actual); + actual.Should().Be(expected); } [Fact] diff --git a/test/RestSharp.Tests/RestClientTests.cs b/test/RestSharp.Tests/RestClientTests.cs index 9d575db4c..0bb54b328 100644 --- a/test/RestSharp.Tests/RestClientTests.cs +++ b/test/RestSharp.Tests/RestClientTests.cs @@ -32,38 +32,6 @@ public async Task ConfigureHttp_will_set_proxy_to_null_with_no_exceptions_When_n await client.ExecuteAsync(req); } - [Fact] - public void BuildUri_should_build_with_passing_link_as_Uri() { - // arrange - var relative = new Uri("/foo/bar/baz", UriKind.Relative); - var absoluteUri = new Uri(new Uri(BaseUrl), relative); - var req = new RestRequest(absoluteUri); - - // act - using var client = new RestClient(); - - var builtUri = client.BuildUri(req); - - // assert - absoluteUri.Should().Be(builtUri); - } - - [Fact] - public void BuildUri_should_build_with_passing_link_as_Uri_with_set_BaseUrl() { - // arrange - var baseUrl = new Uri(BaseUrl); - var relative = new Uri("/foo/bar/baz", UriKind.Relative); - var req = new RestRequest(relative); - - // act - using var client = new RestClient(baseUrl); - - var builtUri = client.BuildUri(req); - - // assert - new Uri(baseUrl, relative).Should().Be(builtUri); - } - [Fact] public void UseJson_leaves_only_json_serializer() { // arrange diff --git a/test/RestSharp.Tests/RestRequestTests.cs b/test/RestSharp.Tests/RestRequestTests.cs index c7e28c702..d9925406d 100644 --- a/test/RestSharp.Tests/RestRequestTests.cs +++ b/test/RestSharp.Tests/RestRequestTests.cs @@ -9,17 +9,26 @@ public void RestRequest_Request_Property() { [Fact] public void RestRequest_Test_Already_Encoded() { - var request = new RestRequest("/api/get?query=Id%3d198&another=notencoded"); + const string resource = "/api/get?query=Id%3d198&another=notencoded&novalue="; + const string baseUrl = "https://example.com"; + + var request = new RestRequest(resource); var parameters = request.Parameters.ToArray(); request.Resource.Should().Be("/api/get"); - parameters.Length.Should().Be(2); + parameters.Length.Should().Be(3); var expected = new[] { new { Name = "query", Value = "Id%3d198", Type = ParameterType.QueryString, Encode = false }, - new { Name = "another", Value = "notencoded", Type = ParameterType.QueryString, Encode = false } + new { Name = "another", Value = "notencoded", Type = ParameterType.QueryString, Encode = false }, + new { Name = "novalue", Value = "", Type = ParameterType.QueryString, Encode = false } }; - parameters.Should().BeEquivalentTo(expected, options => options.ExcludingMissingMembers()); + // parameters.Should().BeEquivalentTo(expected, options => options.ExcludingMissingMembers()); + + using var client = new RestClient(baseUrl); + + var actual = client.BuildUri(request); + actual.AbsoluteUri.Should().Be($"{baseUrl}{resource}"); } [Fact] diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs index df937cddc..06c2f23d8 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -21,13 +21,13 @@ public void GET_with_empty_base_and_resource_containing_tokens() { [Fact] public void GET_with_empty_request() { - var request = new RestRequest(); + var request = new RestRequest(); AssertUri("http://example.com", request, "http://example.com/"); } [Fact] public void GET_with_empty_request_and_bare_hostname() { - var request = new RestRequest(); + var request = new RestRequest(); AssertUri("http://example.com", request, "http://example.com/"); } @@ -173,8 +173,12 @@ public void Should_build_uri_using_selected_encoding() { // utf-8 and iso-8859-1 var request = new RestRequest().AddOrUpdateParameter("town", "Hillerød"); - const string expectedDefaultEncoding = "http://example.com/resource?town=Hiller%c3%b8d"; const string expectedIso89591Encoding = "http://example.com/resource?town=Hiller%f8d"; +#if NET6_0_OR_GREATER + const string expectedDefaultEncoding = "http://example.com/resource?town=Hiller%c3%b8d"; +#else + const string expectedDefaultEncoding = "http://example.com/resource?town=Hiller%C3%B8d"; +#endif AssertUri("http://example.com/resource", request, expectedDefaultEncoding); @@ -236,13 +240,37 @@ public void Should_use_ipv6_address() { actual.AbsoluteUri.Should().Be("https://[fe80::290:e8ff:fe8b:2537]:8443/api/v1/auth"); } + const string BaseUrl = "http://localhost:8888/"; + + [Fact] + public void Should_build_with_passing_link_as_Uri() { + var relative = new Uri("/foo/bar/baz", UriKind.Relative); + var absoluteUri = new Uri(new Uri(BaseUrl), relative); + var req = new RestRequest(absoluteUri); + + AssertUri(req, absoluteUri.AbsoluteUri); + } + + [Fact] + public void Should_build_with_passing_link_as_Uri_with_set_BaseUrl() { + var baseUrl = new Uri(BaseUrl); + var relative = new Uri("/foo/bar/baz", UriKind.Relative); + var req = new RestRequest(relative); + + using var client = new RestClient(baseUrl); + + var builtUri = client.BuildUri(req); + + AssertUri(BaseUrl, req, builtUri.AbsoluteUri); + } + [Fact] public void Should_encode_resource() { - const string baseUrl = "https://example.com"; + const string baseUrl = "https://example.com"; const string resource = "resource?param=value with spaces"; var request = new RestRequest(resource); - var uri = new Uri($"{baseUrl}/{resource}"); + var uri = new Uri($"{baseUrl}/{resource}"); AssertUri(baseUrl, request, uri.AbsoluteUri); } From 04699ff9e117b5c1b237370adadd05cd9404c626 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Mon, 27 May 2024 15:51:58 +0200 Subject: [PATCH 6/6] Ok, it is `dontEscape` so needs to be true. --- src/RestSharp/Request/UriExtensions.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs index 41485467b..9c3df0eb6 100644 --- a/src/RestSharp/Request/UriExtensions.cs +++ b/src/RestSharp/Request/UriExtensions.cs @@ -36,7 +36,9 @@ public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { #if NET6_0_OR_GREATER ? new Uri(assembled, UriOptions) #else - ? new Uri(assembled, false) +#pragma warning disable CS0618 // Type or member is obsolete + ? new Uri(assembled, true) +#pragma warning restore CS0618 // Type or member is obsolete #endif : throw new ArgumentException("Both BaseUrl and Resource are empty", nameof(resource)); } @@ -53,8 +55,10 @@ public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { return assembled != null ? new Uri(isResourceAbsolute ? assembled : $"{usingBaseUri.AbsoluteUri}{assembled}", UriOptions) : baseUrl; #else - var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri($"{baseUrl.AbsoluteUri}/", false); - return assembled != null ? new Uri(usingBaseUri, assembled, false) : baseUrl; +#pragma warning disable CS0618 // Type or member is obsolete + var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri($"{baseUrl.AbsoluteUri}/", true); + return assembled != null ? new Uri(usingBaseUri, assembled, true) : baseUrl; +#pragma warning restore CS0618 // Type or member is obsolete #endif } @@ -66,10 +70,10 @@ public static Uri AddQueryString(this Uri uri, string? query) { var result = #if NET6_0_OR_GREATER - new Uri($"{absoluteUri}{separator}{query}", new UriCreationOptions { DangerousDisablePathAndQueryCanonicalization = true }); + new Uri($"{absoluteUri}{separator}{query}", UriOptions); #else #pragma warning disable CS0618 // Type or member is obsolete - new Uri($"{absoluteUri}{separator}{query}", false); + new Uri($"{absoluteUri}{separator}{query}", true); #pragma warning restore CS0618 // Type or member is obsolete #endif return result;