Skip to content

Commit ea557db

Browse files
Fix pipe char encoding issue (#2345)
* Fix #2207: Prevent Uri constructor from re-encoding pipe character when encode=false The issue was that when AddQueryParameter was called with encode=false, the pipe character (|) was still being encoded to %7C. This happened because the AddQueryString method was creating a new Uri object directly, and the Uri constructor automatically encodes certain characters including the pipe character. The fix uses UriBuilder instead, which preserves the query string as-is without re-encoding it. This ensures that when encode=false is specified, characters like pipe (|) remain unencoded as expected. Added test case to verify pipe character is not encoded when encode=false. * Fix issue #2207: Preserve unencoded characters in query parameters when encode=false - Modified BuildUriExtensions to add BuildUriString method that returns a string URI - Updated RestClient.Async to use string URI for HttpRequestMessage to preserve unencoded characters - Added RawUrl property to RequestBodyCapturer to capture the actual URL string sent over HTTP - Added integration test to verify pipe character is not encoded when encode=false - All existing tests pass * Fix pipe encoding issue * Remove usage of BuildUrl
1 parent 13b0425 commit ea557db

File tree

12 files changed

+68
-34
lines changed

12 files changed

+68
-34
lines changed

src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ internal static void AddOAuthData(
245245
"Using query parameters in the base URL is not supported for OAuth calls. Consider using AddDefaultQueryParameter instead."
246246
);
247247

248-
var url = client.BuildUri(request).ToString();
248+
var url = client.BuildUriString(request);
249249
var queryStringStart = url.IndexOf('?');
250250

251251
if (queryStringStart != -1) url = url[..queryStringStart];

src/RestSharp/BuildUriExtensions.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,23 @@ public static class BuildUriExtensions {
2323
/// </summary>
2424
/// <param name="request">Request instance</param>
2525
/// <returns></returns>
26-
public Uri BuildUri(RestRequest request) {
27-
DoBuildUriValidations(client, request);
26+
public Uri BuildUri(RestRequest request) => new(client.BuildUriString(request));
2827

29-
var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues(
30-
request.Resource,
31-
client.Options.Encode,
32-
request.Parameters,
33-
client.DefaultParameters
34-
);
35-
var mergedUri = uri.MergeBaseUrlAndResource(resource);
28+
/// <summary>
29+
/// Builds the URI string for the request. This method returns a string instead of a Uri object
30+
/// to preserve unencoded characters when encode=false is specified for query parameters.
31+
/// </summary>
32+
/// <param name="request">Request instance</param>
33+
/// <returns></returns>
34+
[PublicAPI]
35+
public string BuildUriString(RestRequest request) {
36+
var mergedUri = client.BuildUriWithoutQueryParameters(request);
3637
var query = client.GetRequestQuery(request);
37-
return mergedUri.AddQueryString(query);
38+
39+
if (query == null) return mergedUri.AbsoluteUri;
40+
41+
var separator = mergedUri.AbsoluteUri.Contains('?') ? "&" : "?";
42+
return $"{mergedUri.AbsoluteUri}{separator}{query}";
3843
}
3944

4045
/// <summary>

src/RestSharp/Request/UriExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) {
4141
public static Uri AddQueryString(this Uri uri, string? query) {
4242
if (query == null) return uri;
4343

44-
var absoluteUri = uri.AbsoluteUri;
45-
var separator = absoluteUri.Contains('?') ? "&" : "?";
44+
var builder = new UriBuilder(uri);
45+
builder.Query = builder.Query.Length > 1 ? $"{builder.Query[1..]}&{query}" : query;
4646

47-
return new($"{absoluteUri}{separator}{query}");
47+
return builder.Uri;
4848
}
4949

5050
public static UrlSegmentParamsValues GetUrlSegmentParamsValues(

src/RestSharp/RestClient.Async.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
111111
using var requestContent = new RequestContent(this, request);
112112

113113
var httpMethod = AsHttpMethod(request.Method);
114-
var url = this.BuildUri(request);
114+
var urlString = this.BuildUriString(request);
115+
var url = new Uri(urlString);
115116

116-
using var message = new HttpRequestMessage(httpMethod, url);
117+
using var message = new HttpRequestMessage(httpMethod, urlString);
117118
message.Content = requestContent.BuildContent();
118119
message.Headers.Host = Options.BaseHost;
119120
message.Headers.CacheControl = request.CachePolicy ?? Options.CachePolicy;

test/RestSharp.InteractiveTests/AuthenticationTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(Twitter
3939

4040
request = new($"oauth/authorize?oauth_token={oauthToken}");
4141

42-
var url = client.BuildUri(request)
43-
.ToString();
42+
var url = client.BuildUriString(request);
4443

4544
Console.WriteLine($"Open this URL in the browser: {url} and complete the authentication.");
4645
Console.Write("Enter the verifier: ");

test/RestSharp.Tests.Integrated/DefaultParameterTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,17 @@ public async Task Should_not_throw_exception_when_name_is_null() {
3535

3636
await client.ExecuteAsync(request);
3737
}
38+
39+
[Fact]
40+
public async Task Should_not_encode_pipe_character_when_encode_is_false() {
41+
using var client = new RestClient(server.Url!);
42+
43+
var request = new RestRequest("capture");
44+
request.AddQueryParameter("ids", "in:001|116", false);
45+
46+
await client.ExecuteAsync(request);
47+
48+
var query = _capturer.RawUrl.Split('?')[1];
49+
query.Should().Contain("ids=in:001|116");
50+
}
3851
}

test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public class RequestBodyCapturer {
77
public bool HasBody { get; private set; }
88
public string Body { get; private set; }
99
public Uri Url { get; private set; }
10+
public string RawUrl { get; private set; }
1011

1112
public bool CaptureBody(string content) {
1213
Body = content;
@@ -23,6 +24,7 @@ public bool CaptureHeaders(IDictionary<string, string[]> headers) {
2324
}
2425

2526
public bool CaptureUrl(string url) {
27+
RawUrl = url;
2628
Url = new(url);
2729
return true;
2830
}

test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void Generates_correct_signature_base() {
3737
var requestParameters = _request.Parameters.ToWebParameters().ToArray();
3838
var parameters = new WebPairCollection();
3939
parameters.AddRange(requestParameters);
40-
var url = _client.BuildUri(_request).ToString();
40+
var url = _client.BuildUriString(_request);
4141
_workflow.RequestUrl = url;
4242
var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters);
4343
oauthParameters.Parameters.AddRange(requestParameters);

test/RestSharp.Tests/Auth/OAuth1Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public async Task Can_Authenticate_OAuth1_With_Querystring_Parameters() {
4646
authenticator.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters;
4747
await authenticator.Authenticate(client, request);
4848

49-
var requestUri = client.BuildUri(request);
49+
var requestUri = new Uri(client.BuildUriString(request));
5050
var actual = requestUri.ParseQuery().Select(x => x.Key).ToList();
5151

5252
actual.Should().BeEquivalentTo(expected);

test/RestSharp.Tests/Parameters/UrlSegmentTests.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
namespace RestSharp.Tests.Parameters;
22

33
public class UrlSegmentTests {
4-
const string BaseUrl = "http://localhost:8888/";
4+
const string BaseUrlNoTrail = "http://localhost:8888";
5+
const string BaseUrl = $"{BaseUrlNoTrail}/";
56

67
[Fact]
78
public void AddUrlSegmentWithInt() {
@@ -22,10 +23,10 @@ public void AddUrlSegmentModifiesUrlSegmentWithInt() {
2223

2324
var path = string.Format(pathTemplate, $"{{{name}}}");
2425
var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue);
25-
var expected = string.Format(pathTemplate, urlSegmentValue);
26+
var expected = $"{BaseUrlNoTrail}{string.Format(pathTemplate, urlSegmentValue)}";
2627

2728
using var client = new RestClient(BaseUrl);
28-
var actual = client.BuildUri(request).AbsolutePath;
29+
var actual = client.BuildUriString(request);
2930

3031
expected.Should().BeEquivalentTo(actual);
3132
}
@@ -38,11 +39,11 @@ public void AddUrlSegmentModifiesUrlSegmentWithString() {
3839

3940
var path = string.Format(pathTemplate, $"{{{name}}}");
4041
var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue);
41-
var expected = string.Format(pathTemplate, urlSegmentValue);
42+
var expected = $"{BaseUrlNoTrail}{string.Format(pathTemplate, urlSegmentValue)}";
4243

4344
using var client = new RestClient(BaseUrl);
4445

45-
var actual = client.BuildUri(request).AbsolutePath;
46+
var actual = client.BuildUriString(request);
4647

4748
expected.Should().BeEquivalentTo(actual);
4849
}
@@ -73,14 +74,15 @@ public void UrlSegmentParameter_WithValueWithEncodedSlash_CanLeaveEncodedSlash(s
7374

7475
[Fact]
7576
public void AddSameUrlSegmentTwice_ShouldReplaceFirst() {
76-
var client = new RestClient();
77-
var request = new RestRequest("https://api.example.com/orgs/{segment}/something");
77+
const string host = "https://api.example.com";
78+
var client = new RestClient();
79+
var request = new RestRequest($"{host}/orgs/{{segment}}/something");
7880
request.AddUrlSegment("segment", 1);
79-
var url1 = client.BuildUri(request);
81+
var url1 = client.BuildUriString(request);
8082
request.AddUrlSegment("segment", 2);
81-
var url2 = client.BuildUri(request);
82-
83-
url1.AbsolutePath.Should().Be("/orgs/1/something");
84-
url2.AbsolutePath.Should().Be("/orgs/2/something");
83+
var url2 = client.BuildUriString(request);
84+
85+
url1.Should().Be($"{host}/orgs/1/something");
86+
url2.Should().Be($"{host}/orgs/2/something");
8587
}
8688
}

0 commit comments

Comments
 (0)