Skip to content

Sync API for HttpClient #32125

@ManickaP

Description

@ManickaP

Provide sync API on HttpClient, which currently has only async methods.

Motivation

Azure SDK currently exposes synchronous API which does sync-over-async on HttpClient (see HttpClientTransport.cs). It would be much better if we provided them with sync API with proper sync implementation, where feasible.

Also there are lots of existing code bases that have very deep synchronous call stacks. Developers are simply not willing to rewrite these code bases to be asynchronous. If they need to call async only API in these synchronous methods, they use sync-over-async, which then in turn causes "soft" deadlock. We want to provide synchronous APIs for these developers because synchronous APIs, although inefficient, can can help in avoiding these soft deadlocks.

Another advantage of sync API is that it is much easier to grasp. Especially for people with no prior knowledge of asynchronous processing. If someone is starting with HttpClient, they also have to be knowledgeable of C# async/await. Also many examples with ``HttpClient` might be simplified and thus making the entry into .NET easier.

Proposed API

Minimal Necessary Change

This change is based on @stephentoub prototype in stephentoub/corefx@0e4d640. The prototype introduces synchronous API for SocketsHttpHandler and also properly implements it for HTTP 1.1 scenarios (for HTTP 2 does sync-over-async). This proposal extends the existing prototype with sync API on HttpClient. Following changes are a minimal set to achieve synchronous HttpClient.Send behaving trully synchronously at least for HTTP 1.1.

Note that CancellationToken is used in synchronous methods in order to propagate HttpClient timeout and cancellations. For HttpContent it's up for discussion whether to add other overload to CopyTo and SerializeToStream to match the async counterparts.

The prototype/early implementation for the minimal change can be found in my branch https://github.com/ManickaP/runtime/tree/mapichov/sync_http_api.

// The main classes where we want the sync API.
public partial class HttpMessageInvoker : IDisposable
{
    ...
    // Existing async method
    public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);

    // Proposed new sync methods
    public virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}
public partial class HttpClient : HttpMessageInvoker
{
    ...
    // Existing async methods
    public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
    public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption);
    public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);
    public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
    
    // Proposed new sync methods
    public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption = default, CancellationToken cancellationToken = default);
    public override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}

// The changes bellow enable proper implementation of the sync API.

// HttpMessageHandler and derived classes
public abstract partial class HttpMessageHandler : IDisposable
{
    ...
    // Existing async method
    protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
    
    // Proposed new sync method
    protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}
public abstract partial class DelegatingHandler : HttpMessageHandler
{
    ...
    // Proposed new sync method
    protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}
public abstract partial class HttpClientHandler : HttpMessageHandler
{
    ...
    // Proposed new sync method
    protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}
public abstract partial class SocketsHttpHandler : HttpMessageHandler
{
    ...
    // Proposed new sync method
    protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}
public abstract partial class MessageProcessingHandler : DelegatingHandler
{
    ...
    // Proposed new sync method
    protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken);
}

// HttpContent and derived classes
public abstract partial class HttpContent : IDisposable
{
    ...
    // Existing async methods
    public Task CopyToAsync(Stream stream);
    public Task CopyToAsync(Stream stream, TransportContext context);
    public Task CopyToAsync(Stream stream, TransportContext context, CancellationToken cancellationToken);
    public Task CopyToAsync(Stream stream, CancellationToken cancellationToken);

    protected abstract Task SerializeToStreamAsync(Stream stream, TransportContext context);
    protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken);

    // Proposed new sync methods
    public void CopyTo(Stream stream, TransportContext context, CancellationToken cancellationToken);

    protected virtual void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken);
}
public partial class ByteArrayContent : HttpContent
{
    ...
    // Proposed new sync method
    protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken);
}
public partial class MultipartContent : HttpContent, IEnumerable<HttpContent>, IEnumerable
{
    ...
    // Proposed new sync method
    protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken);
}
public sealed partial class ReadOnlyMemoryContent : HttpContent
{
    ...
    // Proposed new sync method
    protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken);
}
public partial class StreamContent : HttpContent
{
    ...
    // Proposed new sync method
    protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken);
}

Full Change

This includes sync counterparts to all async HttpClient methods. Since their implementation delegates to Send underneath, there's no need to add any other supporting sync methods.
Note that all the sync methods on HttpClient (except for HttpMessageInvoker.Send override) do not need oveloads with CancellationToken. Whether to include them or not is up for discussion.

public partial class HttpClient : HttpMessageInvoker
{
    ...
    // Existing async methods
    public Task<HttpResponseMessage> DeleteAsync(string requestUri);
    public Task<HttpResponseMessage> DeleteAsync(string requestUri, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> DeleteAsync(Uri requestUri);
    public Task<HttpResponseMessage> DeleteAsync(Uri requestUri, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> GetAsync(string requestUri);
    public Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption);
    public Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> GetAsync(string requestUri, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> GetAsync(Uri requestUri);
    public Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption);
    public Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> GetAsync(Uri requestUri, CancellationToken cancellationToken);
    public Task<byte[]> GetByteArrayAsync(string requestUri);
    public Task<byte[]> GetByteArrayAsync(string requestUri, CancellationToken cancellationToken);
    public Task<byte[]> GetByteArrayAsync(Uri requestUri);
    public Task<byte[]> GetByteArrayAsync(Uri requestUri, CancellationToken cancellationToken);
    public Task<Stream> GetStreamAsync(string requestUri);
    public Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken);
    public Task<Stream> GetStreamAsync(Uri requestUri);
    public Task<Stream> GetStreamAsync(Uri requestUri, CancellationToken cancellationToken);
    public Task<string> GetStringAsync(string requestUri);
    public Task<string> GetStringAsync(string requestUri, CancellationToken cancellationToken);
    public Task<string> GetStringAsync(Uri requestUri);
    public Task<string> GetStringAsync(Uri requestUri, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> PatchAsync(string requestUri, HttpContent content);
    public Task<HttpResponseMessage> PatchAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> PatchAsync(Uri requestUri, HttpContent content);
    public Task<HttpResponseMessage> PatchAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
    public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content);
    public Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
    public Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
    public Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content);
    public Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
    
    // Proposed new sync methods
    public HttpResponseMessage Delete(string requestUri);
    public HttpResponseMessage Delete(string requestUri, CancellationToken cancellationToken);
    public HttpResponseMessage Delete(Uri requestUri);
    public HttpResponseMessage Delete(Uri requestUri, CancellationToken cancellationToken);
    public HttpResponseMessage Get(string requestUri);
    public HttpResponseMessage Get(string requestUri, HttpCompletionOption completionOption);
    public HttpResponseMessage Get(string requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
    public HttpResponseMessage Get(string requestUri, CancellationToken cancellationToken);
    public HttpResponseMessage Get(Uri requestUri);
    public HttpResponseMessage Get(Uri requestUri, HttpCompletionOption completionOption);
    public HttpResponseMessage Get(Uri requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
    public HttpResponseMessage Get(Uri requestUri, CancellationToken cancellationToken);
    public byte[] GetByteArray(string requestUri);
    public byte[] GetByteArray(string requestUri, CancellationToken cancellationToken);
    public byte[] GetByteArray(Uri requestUri);
    public byte[] GetByteArray(Uri requestUri, CancellationToken cancellationToken);
    public Stream GetStream(string requestUri);
    public Stream GetStream(string requestUri, CancellationToken cancellationToken);
    public Stream GetStream(Uri requestUri);
    public Stream GetStream(Uri requestUri, CancellationToken cancellationToken);
    public string GetString(string requestUri);
    public string GetString(string requestUri, CancellationToken cancellationToken);
    public string GetString(Uri requestUri);
    public string GetString(Uri requestUri, CancellationToken cancellationToken);
    public HttpResponseMessage Patch(string requestUri, HttpContent content);
    public HttpResponseMessage Patch(string requestUri, HttpContent content, CancellationToken cancellationToken);
    public HttpResponseMessage Patch(Uri requestUri, HttpContent content);
    public HttpResponseMessage Patch(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
    public HttpResponseMessage Post(string requestUri, HttpContent content);
    public HttpResponseMessage Post(string requestUri, HttpContent content, CancellationToken cancellationToken);
    public HttpResponseMessage Post(Uri requestUri, HttpContent content);
    public HttpResponseMessage Post(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
    public HttpResponseMessage Put(string requestUri, HttpContent content);
    public HttpResponseMessage Put(string requestUri, HttpContent content, CancellationToken cancellationToken);
    public HttpResponseMessage Put(Uri requestUri, HttpContent content);
    public HttpResponseMessage Put(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
}

@stephentoub @KrzysztofCwalina @dotnet/ncl

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions