diff --git a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs index f69f5ff39..56906120f 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs @@ -245,7 +245,7 @@ internal static void AddOAuthData( "Using query parameters in the base URL is not supported for OAuth calls. Consider using AddDefaultQueryParameter instead." ); - var url = client.BuildUri(request).ToString(); + var url = client.BuildUriString(request); var queryStringStart = url.IndexOf('?'); if (queryStringStart != -1) url = url[..queryStringStart]; diff --git a/src/RestSharp/BuildUriExtensions.cs b/src/RestSharp/BuildUriExtensions.cs index 6bf976244..8fb95563e 100644 --- a/src/RestSharp/BuildUriExtensions.cs +++ b/src/RestSharp/BuildUriExtensions.cs @@ -23,18 +23,23 @@ public static class BuildUriExtensions { /// /// Request instance /// - public Uri BuildUri(RestRequest request) { - DoBuildUriValidations(client, request); + public Uri BuildUri(RestRequest request) => new(client.BuildUriString(request)); - var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues( - request.Resource, - client.Options.Encode, - request.Parameters, - client.DefaultParameters - ); - var mergedUri = uri.MergeBaseUrlAndResource(resource); + /// + /// Builds the URI string for the request. This method returns a string instead of a Uri object + /// to preserve unencoded characters when encode=false is specified for query parameters. + /// + /// Request instance + /// + [PublicAPI] + public string BuildUriString(RestRequest request) { + var mergedUri = client.BuildUriWithoutQueryParameters(request); var query = client.GetRequestQuery(request); - return mergedUri.AddQueryString(query); + + if (query == null) return mergedUri.AbsoluteUri; + + var separator = mergedUri.AbsoluteUri.Contains('?') ? "&" : "?"; + return $"{mergedUri.AbsoluteUri}{separator}{query}"; } /// diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs index 9cb26ebe8..bc217dff6 100644 --- a/src/RestSharp/Request/UriExtensions.cs +++ b/src/RestSharp/Request/UriExtensions.cs @@ -41,10 +41,10 @@ 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('?') ? "&" : "?"; + var builder = new UriBuilder(uri); + builder.Query = builder.Query.Length > 1 ? $"{builder.Query[1..]}&{query}" : query; - return new($"{absoluteUri}{separator}{query}"); + return builder.Uri; } public static UrlSegmentParamsValues GetUrlSegmentParamsValues( diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 75c8ac85f..df4558795 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -111,9 +111,10 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo using var requestContent = new RequestContent(this, request); var httpMethod = AsHttpMethod(request.Method); - var url = this.BuildUri(request); + var urlString = this.BuildUriString(request); + var url = new Uri(urlString); - using var message = new HttpRequestMessage(httpMethod, url); + using var message = new HttpRequestMessage(httpMethod, urlString); message.Content = requestContent.BuildContent(); message.Headers.Host = Options.BaseHost; message.Headers.CacheControl = request.CachePolicy ?? Options.CachePolicy; diff --git a/test/RestSharp.InteractiveTests/AuthenticationTests.cs b/test/RestSharp.InteractiveTests/AuthenticationTests.cs index 7e5d81173..72754b1a8 100644 --- a/test/RestSharp.InteractiveTests/AuthenticationTests.cs +++ b/test/RestSharp.InteractiveTests/AuthenticationTests.cs @@ -39,8 +39,7 @@ public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(Twitter request = new($"oauth/authorize?oauth_token={oauthToken}"); - var url = client.BuildUri(request) - .ToString(); + var url = client.BuildUriString(request); Console.WriteLine($"Open this URL in the browser: {url} and complete the authentication."); Console.Write("Enter the verifier: "); diff --git a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs index f61c5a55d..c2038f6a3 100644 --- a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs +++ b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs @@ -35,4 +35,17 @@ public async Task Should_not_throw_exception_when_name_is_null() { await client.ExecuteAsync(request); } + + [Fact] + public async Task Should_not_encode_pipe_character_when_encode_is_false() { + using var client = new RestClient(server.Url!); + + var request = new RestRequest("capture"); + request.AddQueryParameter("ids", "in:001|116", false); + + await client.ExecuteAsync(request); + + var query = _capturer.RawUrl.Split('?')[1]; + query.Should().Contain("ids=in:001|116"); + } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs index cc9ead4fb..cca83ff9c 100644 --- a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs +++ b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs @@ -7,6 +7,7 @@ public class RequestBodyCapturer { public bool HasBody { get; private set; } public string Body { get; private set; } public Uri Url { get; private set; } + public string RawUrl { get; private set; } public bool CaptureBody(string content) { Body = content; @@ -23,6 +24,7 @@ public bool CaptureHeaders(IDictionary headers) { } public bool CaptureUrl(string url) { + RawUrl = url; Url = new(url); return true; } diff --git a/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs b/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs index 782a7a121..f23f411c5 100644 --- a/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs +++ b/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs @@ -37,7 +37,7 @@ public void Generates_correct_signature_base() { var requestParameters = _request.Parameters.ToWebParameters().ToArray(); var parameters = new WebPairCollection(); parameters.AddRange(requestParameters); - var url = _client.BuildUri(_request).ToString(); + var url = _client.BuildUriString(_request); _workflow.RequestUrl = url; var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters); oauthParameters.Parameters.AddRange(requestParameters); diff --git a/test/RestSharp.Tests/Auth/OAuth1Tests.cs b/test/RestSharp.Tests/Auth/OAuth1Tests.cs index 483a89771..8f4356cf6 100644 --- a/test/RestSharp.Tests/Auth/OAuth1Tests.cs +++ b/test/RestSharp.Tests/Auth/OAuth1Tests.cs @@ -46,7 +46,7 @@ public async Task Can_Authenticate_OAuth1_With_Querystring_Parameters() { authenticator.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters; await authenticator.Authenticate(client, request); - var requestUri = client.BuildUri(request); + var requestUri = new Uri(client.BuildUriString(request)); var actual = requestUri.ParseQuery().Select(x => x.Key).ToList(); actual.Should().BeEquivalentTo(expected); diff --git a/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs b/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs index 5282dc57c..d7358a333 100644 --- a/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs +++ b/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs @@ -1,7 +1,8 @@ namespace RestSharp.Tests.Parameters; public class UrlSegmentTests { - const string BaseUrl = "http://localhost:8888/"; + const string BaseUrlNoTrail = "http://localhost:8888"; + const string BaseUrl = $"{BaseUrlNoTrail}/"; [Fact] public void AddUrlSegmentWithInt() { @@ -22,10 +23,10 @@ public void AddUrlSegmentModifiesUrlSegmentWithInt() { var path = string.Format(pathTemplate, $"{{{name}}}"); var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue); - var expected = string.Format(pathTemplate, urlSegmentValue); + var expected = $"{BaseUrlNoTrail}{string.Format(pathTemplate, urlSegmentValue)}"; using var client = new RestClient(BaseUrl); - var actual = client.BuildUri(request).AbsolutePath; + var actual = client.BuildUriString(request); expected.Should().BeEquivalentTo(actual); } @@ -38,11 +39,11 @@ public void AddUrlSegmentModifiesUrlSegmentWithString() { var path = string.Format(pathTemplate, $"{{{name}}}"); var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue); - var expected = string.Format(pathTemplate, urlSegmentValue); + var expected = $"{BaseUrlNoTrail}{string.Format(pathTemplate, urlSegmentValue)}"; using var client = new RestClient(BaseUrl); - var actual = client.BuildUri(request).AbsolutePath; + var actual = client.BuildUriString(request); expected.Should().BeEquivalentTo(actual); } @@ -73,14 +74,15 @@ public void UrlSegmentParameter_WithValueWithEncodedSlash_CanLeaveEncodedSlash(s [Fact] public void AddSameUrlSegmentTwice_ShouldReplaceFirst() { - var client = new RestClient(); - var request = new RestRequest("https://api.example.com/orgs/{segment}/something"); + const string host = "https://api.example.com"; + var client = new RestClient(); + var request = new RestRequest($"{host}/orgs/{{segment}}/something"); request.AddUrlSegment("segment", 1); - var url1 = client.BuildUri(request); + var url1 = client.BuildUriString(request); request.AddUrlSegment("segment", 2); - var url2 = client.BuildUri(request); - - url1.AbsolutePath.Should().Be("/orgs/1/something"); - url2.AbsolutePath.Should().Be("/orgs/2/something"); + var url2 = client.BuildUriString(request); + + url1.Should().Be($"{host}/orgs/1/something"); + url2.Should().Be($"{host}/orgs/2/something"); } } \ No newline at end of file diff --git a/test/RestSharp.Tests/RestRequestTests.cs b/test/RestSharp.Tests/RestRequestTests.cs index 988f91734..dc2053c09 100644 --- a/test/RestSharp.Tests/RestRequestTests.cs +++ b/test/RestSharp.Tests/RestRequestTests.cs @@ -26,8 +26,8 @@ public void RestRequest_Test_Already_Encoded() { parameters.Should().BeEquivalentTo(expected, options => options.ExcludingMissingMembers()); using var client = new RestClient(baseUrl); - var actual = client.BuildUri(request); - actual.AbsoluteUri.Should().Be($"{baseUrl}{resource}"); + var actual = client.BuildUriString(request); + actual.Should().Be($"{baseUrl}{resource}"); } [Fact] diff --git a/test/RestSharp.Tests/UrlBuilderTests.Get.cs b/test/RestSharp.Tests/UrlBuilderTests.Get.cs index 85c04b35e..4a2909697 100644 --- a/test/RestSharp.Tests/UrlBuilderTests.Get.cs +++ b/test/RestSharp.Tests/UrlBuilderTests.Get.cs @@ -60,6 +60,18 @@ public void GET_with_empty_request_and_query_parameters_without_encoding() { Assert.Equal(expected, output); } + [Fact] + public void GET_with_pipe_character_in_query_parameter_without_encoding() { + var request = new RestRequest(); + request.AddQueryParameter("ids", "in:001|116", false); + const string expected = $"{Base}/{Resource}?ids=in:001|116"; + + using var client = new RestClient($"{Base}/{Resource}"); + + var output = client.BuildUriString(request); + Assert.Equal(expected, output); + } + [Fact] public void GET_with_Invalid_Url_string_throws_exception() => Assert.Throws(() => { _ = new RestClient("invalid url"); }