Skip to content

Configuring request options in Browser WebAssembly #34168

Closed

Description

@pranavkm

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 json

Additional 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

  1. Cache (https://developer.mozilla.org/en-US/docs/Web/API/Request/cache)

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.

  1. Integrity (https://developer.mozilla.org/en-US/docs/Web/API/Request/integrity)

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.

  1. Credentials (https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)

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.

  1. 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

  1. 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);
  1. 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);
  1. 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(...)

Activity

  1. added
    blockingMarks issues that we want to fast track in order to unblock other important work
    on Mar 26, 2020
  2. davidsh commented on Mar 27, 2020

    @davidsh
    Contributor
  3. pranavkm commented on Mar 27, 2020

    @pranavkm
    ContributorAuthor

    The current implementation is located here: https://github.com/mono/mono/blob/master/sdks/wasm/framework/src/WebAssembly.Net.Http/WasmHttpMessageHandler.cs. Work's being done to rename the type and add these options as part of mono/mono#19260.

    /cc @marek-safar / @kjpou1 / @lewing / @terrajobst

  4. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    and removed on Mar 27, 2020
  5. scalablecory commented on Mar 30, 2020

    @scalablecory
    Contributor
    namespace System.Net.Http

    Given this type is very platform-specific, does it make sense have it in the System namespace or should it be somewhere else?

            // https://developer.mozilla.org/en-US/docs/Web/API/Request/integrity
            public string? Integrity { get; set; }

    It looks like this should be read-only based on the Mozilla docs. It appears to never return null there -- will we be normalizing "" into null for this API?

            // https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
            public RequestCache? RequestCache { get; set; }

    It looks like this doesn't need to be nullable; RequestCache.Default is fine.

            // https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
            public RequestCredentials? RequestCredentials { get; set; }

    Our other handlers have a ICredentials Credentials property. Would it make any sense to use the same here and have some sentinel values instead of an enum? We have precedent with sentinal implementations in CredentialCache.DefaultCredentials and CredentialCache.DefaultNetworkCredentials, and this would leave the door open for less browsery credential implementations in the future.

            // 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; }

    Should this be static read-only? We have other APIs that use "Supports" instead of "Enabled"; may want to use that here.

  6. scalablecory commented on Mar 30, 2020

    @scalablecory
    Contributor

    Also, it looks like all of these APIs are at the request level in fetch, not the handler level (though fetch does not have a handler). We should think about how that shapes this here.

    Maybe a derived HttpRequestMessage makes some sense, though I can see that causing hurdles for 3rd party libraries.

  7. added and removed
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Mar 30, 2020
  8. pranavkm commented on Mar 30, 2020

    @pranavkm
    ContributorAuthor

    Given this type is very platform-specific, does it make sense have it in the System namespace or should it be somewhere else?

    Based on an offline discussion, the plan was to mirror the platform-specific WinHttpHandler. I chose this namespaces to keep with the pattern there.

    It looks like this should be read-only based on the Mozilla docs. It appears to never return null there -- will we be normalizing "" into null for this API?

    The API the handler is attempting to mirror is window.fetch which allows configuring these values - https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax. A usage for this would look like:

    await fetch(myUri, { integrity: 'some-value' })

    I wanted to leave these unspecified rather than specify a default, in the event the spec gets changed to have a new default at a future point.

    We have precedent with sentinal implementations in CredentialCache.DefaultCredentials and CredentialCache.DefaultNetworkCredentials, and this would leave the door open for less browsery credential implementations in the future.

    That's an interesting suggestion. The implementation could set the credentials option if it's one of the sentinel credentials, otherwise set the Authentication header. I'm not 100% certain if there's ever a need the header and the fetch option simultaneously, I'll ask around.

    Should this be static read-only? We have other APIs that use "Supports" instead of "Enabled"; may want to use that here.

    It's meant to be an option that influences whether HttpResponseMessage.Content is an unbuffered stream versus a buffered byte array. The unbuffered stream is required to enable application models that require streaming (such as gRPC). However, having it on by default or making it an implementation detail is problematic - performing a sync read on the returned stream in .NET deadlocks the application (.NET Wasm is single threaded). The property is meant to allow users to opt-in to using streams. In JavaScript, this is what the usage of the two APIs look like:

  9. 24 remaining items

  10. terrajobst commented on Jul 6, 2020

    @terrajobst
    Contributor

    @stephentoub @karelz

    (I'm also surprised to see it marked api-approved; I thought the whole discussion was contingent on being reviewed by @karelz and co.)

    The goal was to unblock people, which is why I told @scalablecory to remove api-ready-for-review if he disagrees with the proposal.

  11. terrajobst commented on Jul 6, 2020

    @terrajobst
    Contributor

    Bumped it back to ready-for-review. @karelz thinks @dotnet/ncl's review should be able to get to this by next week.

    @pranavkm please don't edit the API within a comment that represents approval if they are net-new suggestions. These comments are meant to express what was reviewed/approved at a given point in time. Otherwise it will confuse us in the next meeting :-)

    Instead, update the issue description. I just did that and put the previous ones under a <details> tag.

  12. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    blockingMarks issues that we want to fast track in order to unblock other important work
    on Jul 7, 2020
  13. terrajobst commented on Jul 7, 2020

    @terrajobst
    Contributor

    Video

    • Looks good as proposed
    namespace System.Net.Http
    {
        public readonly struct HttpRequestOptionsKey<TValue>
        {
            public HttpRequestOptionsKey(string 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<TValue>(HttpRequestOptionsKey<TValue> key, TValue value);
        }
        public class HttpRequestMessage : IDisposable
        {
            [Obsolete("Use Options instead.")]
            public IDictionary<string, object> Properties => Options;
    
            public HttpRequestOptions Options { get; }
        }
    }
  14. added this to the 5.0.0 milestone on Jul 13, 2020
  15. marek-safar commented on Jul 30, 2020

    @marek-safar
    Contributor

    Implemented in master

  16. ghost locked as resolved and limited conversation to collaborators on Dec 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions