-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Motivation
Before we look at our requirement in .NET, let's look at fetch. .NET WebAssembly implementation for HttpClient uses Browser's "fetch API" as it's transport.
In it's simplest format, all you need to perform a HTTP request using fetch is to specify a url:
const response = await fetch('products/get'); // Perform a GET request to products/get
const products = await response.json(); // Read the response body as jsonAdditional options to configure the request are passed in a secondary options type:
const response = await fetch('products/post', {
method: 'POST',
body: valueToSend,
headers: {
'Content-Type': 'application/json'
}
});fetch does not have a way to options that apply globally or to more than one request, all options must be configured per-request. Application developers may use helper methods for options that need to be configured frequently (for eg https://github.com/moll/js-fetch-defaults#using).
Let's see what this looks like when translated to using HttpRequestMessage:
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "products/post");
requestMessage.Content = valueToSend;
requestMessage.Content.Headers.ContentType = "application/json";In addition to the method, body and headers parameters, fetch has a few other options that are documented here: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax. In the example above, it was fairly easy to construct a fetch request given a HttpRequestMessage since all of these properties were available on the message instance. With HttpClient's programming model, some features are configurable per request, but many are not. In particular, platform-specific settings are always part of the message handler. We'll look at some common cases in webassembly where we really want per-request control of some of these platform-specific settings
Browsers cache results and will return results from the caches. fetch has ways to force the browser to ignore locally cached resources and perform requests. Cache-busting is an important tool in a web developer's arsenal.
// In this sample, we ensure the catalog is always updated from the server.
const response = await fetch('products/catalog.json', { cache: 'no-cache' });If an equivalent option is offered to .NET WebAssembly developers, it is essential that it is available as a per-request setting. Changing cache settings globally is not desirable.
Subresource integrity is a browser security feature that allows a response to only be read if the contents match the specified hash. It ensures that content downloaded to the browser haven't been tampered with in anyway. This is typically valuable if an application downloaded content from a 3rd party hosted site. For e.g.
// The application defers downloading a large payload until it's necessary. It uses integrity to ensure the contents are as expected.
if (shouldShowGrid()) {
const data = await fetch('https://raw.githubusercontent.com/mydata/large.data.json', { integrity: 'precomuted-sha-goes-here' }).then(r => r.json());
showGrid(data);
}Like the cache option, integrity must be a per-request option.
When performing a fetch request, browsers will default to not including the cookie header as part of the request. If your site relies on cookies for authentication, you need these to included. The credentials option allows configuring these:
const response = await fetch('products/get', { credentials: 'include' });In .NET, credentials are configured on the handler. We could conceive of ways to solving this with what's already present in the HttpClient API, such as by using multiple HttpClient instances, or a handler that conditionally configures credentials. However, being able to configure a request setting on a per-request basis is very convenient.
- Streamed versus buffered responses
The response returned from a fetch operation has methods to read it as bytes, strings, json etc. In addition, browsers allow reading the body as a raw (unbuffered) stream. Some applications such as gRPC Web's server streaming feature require streaming responses.
.NET WebAssembly has an implementation of StreamContent over the response body. When they attempted to make it the default, they received user feedback stating that performing sync reads on this content would result in application deadlocks (WASM is single-threaded). There was enough feedback where they feel the need to make returning an unbuffered stream content an option that users have to opt-into.
API Proposal
namespace System.Net.Http
{
public readonly struct HttpRequestOptionsKey<TValue>
{
public HttpRequestOptionsKey(string key)
{
Key = key;
}
public string Key { get; }
}
public sealed class HttpRequestOptions : IDictionary<string, object>
{
// Explicit interface implementation
public bool TryGetValue<TValue>(HttpRequestOptionsKey<TValue> key, out TValue value);
public void Set(HttpRequestOptionsKey<TValue> key, TValue value);
}
public class HttpRequestMessage : IDisposable
{
[Obsolete("Use Options instead.")]
[EditorBrowseable(Never)]
public IDictionary<string, object> Properties => Options;
public HttpRequestOptions Options { get; }
}
}Previous Proposal 2
+ // Inspired by https://github.com/dotnet/runtime/issues/1793
+ interface IHttpRequestOptions
+ {
+ bool TryGet<TValue>(HttpRequestOptionsKey<TValue> key, TValue value);
+ }
+
+
+ class HttpRequestOptions : IHttpRequestOptions
+ {
+ HttpRequestOptions Add(HttpRequestOptionsKey<TValue> key, TValue value);
+ }
// Limiting this list for the sake of brevity.
public partial class HttpClient : System.Net.Http.HttpMessageInvoker
{
public System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> DeleteAsync(System.Uri? requestUri, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> DeleteAsync(System.Uri? requestUri, System.Net.Http.HttpRequestOptions requestOptions, System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> GetAsync(System.Uri? requestUri, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> GetAsync(System.Uri? requestUri, System.Net.Http.HttpRequestOptions requestOptions, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption, System.Net.Http.HttpRequestOptions requestOptions, System.Threading.CancellationToken cancellationToken) { throw null; }
}
+ static System.Threading.Tasks.Task<object?> GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, System.Type type, System.Net.Http.HttpRequestOptions requestOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
partial class HttpRequestMessage : System.IDisposable
{
+ public IHttpRequestOptions RequestOptions { get { throw null; } }
}Usage
- Manually constructing a HttpRequestMessage
var request = new HttpRequestMessage(HttpMethod.Get, "products/catalog.json");
request.Options.Add(BrowserRequestOptions.CacheKey, BrowserRequestCache.NoCache);
// OR convenience extension methods if we deem necessary
request.Options.SetBrowserRequestCache(BrowserRequestCache.NoCache);- HTTP request performed by library or helper code.
var requestOptions = new HttpRequestOptions()
.Add(BrowserRequestOptions.IntegrityKey, "precomuted-sha-goes-here");
var request = await httpClient.GetFromJsonAsync("https://raw.githubusercontent.com/mydata/large.data.json", requestOptions);- HttpRequestMessage constructed by external library
var requestOptions = new HttpRequestOptions();
requestOptions.Add(BrowserRequestOptions.Credentials, BrowserRequestCredentials.Include);
var request = myLibrary.CreateRequest(...);
httpClient.SendAsync(request, requestOptions, cancellationToken);Previous Proposal 1
WebAssembly's HttpClient uses the Fetch API as a transport for HTTP. Similar to how WinHttpHandler allow fine-grained platform specific config, there are options on fetch that a user may want to configure. This API introduces a WebAssemblyHttpHandler that supports this scenario.
Proposed API
namespace System.Net.Http
{
public partial class WebAssemblyHttpHandler : System.Net.Http.HttpMessageHandler
{
public WebAssemblyHttpHandler() { }
// https://developer.mozilla.org/en-US/docs/Web/API/Request/integrity
public string? Integrity { get; set; }
// https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
public RequestCache? RequestCache { get; set; }
// https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
public RequestCredentials? RequestCredentials { get; set; }
// https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
public RequestMode? RequestMode { get; set; }
// Determines if the handler streams the response when supported by the platform.
// See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Concepts.
public bool StreamingEnabled { get; set; }
}
enum RequestCache
{
Default = 0,
NoStore = 1,
Reload = 2,
NoCache = 3,
ForceCache = 4,
OnlyIfCached = 5,
}
enum RequestCredentials
{
Omit = 0,
SameOrigin = 1,
Include = 2,
}
enum RequestMode
{
SameOrigin = 0,
NoCors = 1,
Cors = 2,
Navigate = 3,
}
}Example
var client = new HttpClient(new WebAssemblyHttpHandler
{
RequestCredentials = RequestCredentials.Include,
RequestMode = RequestMode.Cors,
});
client.SendAsync(...)