Skip to content

Commit c362621

Browse files
Add missing ConfigureAwait(false) to prevent deadlocks (#2367)
* Add missing ConfigureAwait(false) to prevent sync-over-async deadlocks Fixes #2083 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add regression tests for sync-over-async deadlock fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a763576 commit c362621

4 files changed

Lines changed: 62 additions & 5 deletions

File tree

src/RestSharp/Extensions/HttpResponseExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static async Task<string> GetResponseString(this HttpResponseMessage resp
3232
var encoding = encodingString != null ? TryGetEncoding(encodingString) : clientEncoding;
3333

3434
using var reader = new StreamReader(new MemoryStream(bytes), encoding);
35-
return await reader.ReadToEndAsync();
35+
return await reader.ReadToEndAsync().ConfigureAwait(false);
3636
Encoding TryGetEncoding(string es) {
3737
try {
3838
return Encoding.GetEncoding(es);

src/RestSharp/Response/RestResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async Task<RestResponse> GetDefaultResponse() {
5454
#endif
5555

5656
var bytes = stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false);
57-
var content = bytes == null ? null : await httpResponse.GetResponseString(bytes, options.Encoding);
57+
var content = bytes == null ? null : await httpResponse.GetResponseString(bytes, options.Encoding).ConfigureAwait(false);
5858

5959
return new(request) {
6060
Content = content,

src/RestSharp/RestClient.Extensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public async Task<RestResponse<T>> ExecuteAsync<T>(
4545
Ensure.NotNull(request, nameof(request));
4646

4747
var response = await client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
48-
return await client.Serializers.Deserialize<T>(request, response, client.Options, cancellationToken);
48+
return await client.Serializers.Deserialize<T>(request, response, client.Options, cancellationToken).ConfigureAwait(false);
4949
}
5050

5151
/// <summary>
@@ -172,9 +172,9 @@ [EnumeratorCancellation] CancellationToken cancellationToken
172172
using var reader = new StreamReader(stream);
173173

174174
#if NET7_0_OR_GREATER
175-
while (await reader.ReadLineAsync(cancellationToken) is { } line && !cancellationToken.IsCancellationRequested) {
175+
while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line && !cancellationToken.IsCancellationRequested) {
176176
#else
177-
while (await reader.ReadLineAsync() is { } line && !cancellationToken.IsCancellationRequested) {
177+
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line && !cancellationToken.IsCancellationRequested) {
178178
#endif
179179
if (string.IsNullOrWhiteSpace(line)) continue;
180180

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
namespace RestSharp.Tests.Integrated;
2+
3+
#pragma warning disable xUnit1031 // Blocking calls in tests are intentional — we are testing sync-over-async deadlock safety
4+
5+
public sealed class SyncRequestTests(WireMockTestServer server) : IClassFixture<WireMockTestServer> {
6+
[Fact]
7+
public void Sync_execute_should_not_deadlock() {
8+
// Regression test for https://github.com/restsharp/RestSharp/issues/2083
9+
// Sync methods (ExecuteGet) could deadlock when await calls inside the pipeline
10+
// did not use ConfigureAwait(false), causing continuations to try to marshal
11+
// back to a captured SynchronizationContext.
12+
13+
using var client = new RestClient(server.Url!);
14+
var request = new RestRequest("success");
15+
16+
RestResponse? response = null;
17+
18+
var completed = Task.Run(() => {
19+
response = client.ExecuteGet(request);
20+
}).Wait(TimeSpan.FromSeconds(10));
21+
22+
completed.Should().BeTrue("sync ExecuteGet should complete without deadlocking");
23+
response.Should().NotBeNull();
24+
response!.IsSuccessStatusCode.Should().BeTrue();
25+
}
26+
27+
[Fact]
28+
public void Sync_execute_with_deserialization_should_not_deadlock() {
29+
using var client = new RestClient(server.Url!);
30+
var request = new RestRequest("success");
31+
32+
RestResponse<SuccessResponse>? response = null;
33+
34+
var completed = Task.Run(() => {
35+
response = client.ExecuteGet<SuccessResponse>(request);
36+
}).Wait(TimeSpan.FromSeconds(10));
37+
38+
completed.Should().BeTrue("sync ExecuteGet<T> should complete without deadlocking");
39+
response.Should().NotBeNull();
40+
response!.IsSuccessStatusCode.Should().BeTrue();
41+
response.Data.Should().NotBeNull();
42+
}
43+
44+
[Fact]
45+
public void Sync_execute_from_multiple_threads_should_not_deadlock() {
46+
using var client = new RestClient(server.Url!);
47+
const int threadCount = 5;
48+
49+
var completed = Parallel.For(0, threadCount, _ => {
50+
var request = new RestRequest("success");
51+
var response = client.ExecuteGet(request);
52+
response.IsSuccessStatusCode.Should().BeTrue();
53+
});
54+
55+
completed.IsCompleted.Should().BeTrue();
56+
}
57+
}

0 commit comments

Comments
 (0)