Fix recently introduced warning. (#11257)
[mono-project.git] / sdks / wasm / WasmHttpMessageHandler.cs
blob826772d9c5306f67b3fc46cad14991e43a30407d
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Net;
5 using System.Net.Http;
6 using System.Threading;
7 using System.Threading.Tasks;
9 namespace WebAssembly.Net.Http.HttpClient
11 public class WasmHttpMessageHandler : HttpMessageHandler
14 static JSObject json;
15 static JSObject fetch;
16 static JSObject window;
17 static JSObject global;
19 /// <summary>
20 /// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
21 /// Defaults to <see cref="FetchCredentialsOption.SameOrigin"/>.
22 /// </summary>
23 public static FetchCredentialsOption DefaultCredentials { get; set; }
24 = FetchCredentialsOption.SameOrigin;
26 public static RequestCache Cache { get; set; }
27 = RequestCache.Default;
29 public static RequestMode Mode { get; set; }
30 = RequestMode.Cors;
32 public WasmHttpMessageHandler()
34 handlerInit();
37 private void handlerInit()
39 window = (JSObject)WebAssembly.Runtime.GetGlobalObject("window");
40 json = (JSObject)WebAssembly.Runtime.GetGlobalObject("JSON");
41 fetch = (JSObject)WebAssembly.Runtime.GetGlobalObject("fetch");
43 // install our global hook to create a Headers object.
44 Runtime.InvokeJS(@"
45 BINDING.mono_wasm_get_global()[""__mono_wasm_headers_hook__""] = function () { return new Headers(); }
46 ");
48 global = (JSObject)WebAssembly.Runtime.GetGlobalObject("");
52 protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
54 var tcs = new TaskCompletionSource<HttpResponseMessage>();
55 cancellationToken.Register(() => tcs.TrySetCanceled());
57 #pragma warning disable 4014
58 doFetch(tcs, request).ConfigureAwait(false);
59 #pragma warning restore 4014
61 return await tcs.Task;
64 private async Task doFetch(TaskCompletionSource<HttpResponseMessage> tcs, HttpRequestMessage request)
67 try
69 var requestObject = (JSObject)json.Invoke("parse", "{}");
70 requestObject.SetObjectProperty("method", request.Method.Method);
72 // See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
73 // standard values and meanings
74 requestObject.SetObjectProperty("credentials", DefaultCredentials);
76 // See https://developer.mozilla.org/en-US/docs/Web/API/Request/cache for
77 // standard values and meanings
78 requestObject.SetObjectProperty("cache", Cache);
80 // See https://developer.mozilla.org/en-US/docs/Web/API/Request/mode for
81 // standard values and meanings
82 requestObject.SetObjectProperty("mode", Mode);
84 // We need to check for body content
85 if (request.Content != null)
87 if (request.Content is StringContent)
89 requestObject.SetObjectProperty("body", await request.Content.ReadAsStringAsync());
91 else
93 requestObject.SetObjectProperty("body", await request.Content.ReadAsByteArrayAsync());
97 // Process headers
98 // Cors has it's own restrictions on headers.
99 // https://developer.mozilla.org/en-US/docs/Web/API/Headers
100 var requestHeaders = GetHeadersAsStringArray(request);
102 if (requestHeaders != null && requestHeaders.Length > 0)
104 using (var jsHeaders = (JSObject)global.Invoke("__mono_wasm_headers_hook__"))
106 for (int i = 0; i < requestHeaders.Length; i++)
108 //Console.WriteLine($"append: {requestHeaders[i][0]} / {requestHeaders[i][1]}");
109 jsHeaders.Invoke("append", requestHeaders[i][0], requestHeaders[i][1]);
111 requestObject.SetObjectProperty("headers", jsHeaders);
115 var args = (JSObject)json.Invoke("parse", "[]");
116 args.Invoke("push", request.RequestUri.ToString());
117 args.Invoke("push", requestObject);
119 requestObject.Dispose();
121 var response = (Task<object>)fetch.Invoke("apply", window, args);
122 args.Dispose();
124 var t = await response;
126 var status = new WasmFetchResponse((JSObject)t);
128 //Console.WriteLine($"bodyUsed: {status.IsBodyUsed}");
129 //Console.WriteLine($"ok: {status.IsOK}");
130 //Console.WriteLine($"redirected: {status.IsRedirected}");
131 //Console.WriteLine($"status: {status.Status}");
132 //Console.WriteLine($"statusText: {status.StatusText}");
133 //Console.WriteLine($"type: {status.ResponseType}");
134 //Console.WriteLine($"url: {status.Url}");
135 byte[] buffer = null;
136 buffer = (byte[])await status.ArrayBuffer();
138 HttpResponseMessage httpresponse = new HttpResponseMessage((HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), status.Status.ToString()));
140 if (buffer != null)
141 httpresponse.Content = new ByteArrayContent(buffer);
143 buffer = null;
145 // Fill the response headers
146 // CORS will only allow access to certain headers.
147 // If a request is made for a resource on another origin which returns the CORs headers, then the type is cors.
148 // cors and basic responses are almost identical except that a cors response restricts the headers you can view to
149 // `Cache-Control`, `Content-Language`, `Content-Type`, `Expires`, `Last-Modified`, and `Pragma`.
150 // View more information https://developers.google.com/web/updates/2015/03/introduction-to-fetch#response_types
152 // Note: Some of the headers may not even be valid header types in .NET thus we use TryAddWithoutValidation
153 using (var respHeaders = status.Headers)
155 // Here we invoke the forEach on the headers object
156 // Note: the Action takes 3 objects and not two. The other seems to be the Header object.
157 respHeaders.Invoke("forEach", new Action<object, object, object>((value, name, other) =>
160 if (!httpresponse.Headers.TryAddWithoutValidation((string)name, (string)value))
161 if (httpresponse.Content != null)
162 if (!httpresponse.Content.Headers.TryAddWithoutValidation((string)name, (string)value))
163 Console.WriteLine($"Warning: Can not add response header for name: {name} value: {value}");
164 ((JSObject)other).Dispose();
170 tcs.SetResult(httpresponse);
172 httpresponse = null;
174 status.Dispose();
176 catch (Exception exception)
178 tcs.SetException(exception);
184 private string[][] GetHeadersAsStringArray(HttpRequestMessage request)
185 => (from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
186 from headerValue in header.Value // There can be more than one value for each name
187 select new[] { header.Key, headerValue }).ToArray();
189 class WasmFetchResponse : IDisposable
191 private JSObject managedJSObject;
192 private int JSHandle;
194 public WasmFetchResponse(JSObject jsobject)
196 managedJSObject = jsobject;
197 JSHandle = managedJSObject.JSHandle;
200 public bool IsOK => (bool)managedJSObject.GetObjectProperty("ok");
201 public bool IsRedirected => (bool)managedJSObject.GetObjectProperty("redirected");
202 public int Status => (int)managedJSObject.GetObjectProperty("status");
203 public string StatusText => (string)managedJSObject.GetObjectProperty("statusText");
204 public string ResponseType => (string)managedJSObject.GetObjectProperty("type");
205 public string Url => (string)managedJSObject.GetObjectProperty("url");
206 //public bool IsUseFinalURL => (bool)managedJSObject.GetObjectProperty("useFinalUrl");
207 public bool IsBodyUsed => (bool)managedJSObject.GetObjectProperty("bodyUsed");
208 public JSObject Headers => (JSObject)managedJSObject.GetObjectProperty("headers");
210 public Task<object> ArrayBuffer() => (Task<object>)managedJSObject.Invoke("arrayBuffer");
211 public Task<object> Text() => (Task<object>)managedJSObject.Invoke("text");
212 public Task<object> JSON() => (Task<object>)managedJSObject.Invoke("json");
214 public void Dispose()
216 // Dispose of unmanaged resources.
217 Dispose(true);
218 // Suppress finalization.
219 GC.SuppressFinalize(this);
222 // Protected implementation of Dispose pattern.
223 protected virtual void Dispose(bool disposing)
226 if (disposing)
229 // Free any other managed objects here.
233 // Free any unmanaged objects here.
235 managedJSObject?.Dispose();
236 managedJSObject = null;
243 /// <summary>
244 /// Specifies a value for the 'credentials' option on outbound HTTP requests.
245 /// </summary>
246 public enum FetchCredentialsOption
248 /// <summary>
249 /// Advises the browser never to send credentials (such as cookies or HTTP auth headers).
250 /// </summary>
251 [Export(EnumValue = ConvertEnum.ToLower)]
252 Omit,
254 /// <summary>
255 /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
256 /// only if the target URL is on the same origin as the calling application.
257 /// </summary>
258 [Export("same-origin")]
259 SameOrigin,
261 /// <summary>
262 /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
263 /// even for cross-origin requests.
264 /// </summary>
265 [Export(EnumValue = ConvertEnum.ToLower)]
266 Include,
270 /// <summary>
271 /// The cache mode of the request. It controls how the request will interact with the browser's HTTP cache.
272 /// </summary>
273 public enum RequestCache
275 /// <summary>
276 /// The browser looks for a matching request in its HTTP cache.
277 /// </summary>
278 [Export(EnumValue = ConvertEnum.ToLower)]
279 Default,
281 /// <summary>
282 /// The browser fetches the resource from the remote server without first looking in the cache,
283 /// and will not update the cache with the downloaded resource.
284 /// </summary>
285 [Export("no-store")]
286 NoStore,
288 /// <summary>
289 /// The browser fetches the resource from the remote server without first looking in the cache,
290 /// but then will update the cache with the downloaded resource.
291 /// </summary>
292 [Export(EnumValue = ConvertEnum.ToLower)]
293 Reload,
295 /// <summary>
296 /// The browser looks for a matching request in its HTTP cache.
297 /// </summary>
298 [Export("no-cache")]
299 NoCache,
301 /// <summary>
302 /// The browser looks for a matching request in its HTTP cache.
303 /// </summary>
304 [Export("force-cache")]
305 ForceCache,
307 /// <summary>
308 /// The browser looks for a matching request in its HTTP cache.
309 /// Mode can only be used if the request's mode is "same-origin"
310 /// </summary>
311 [Export("only-if-cached")]
312 OnlyIfCached,
315 /// <summary>
316 /// The mode of the request. This is used to determine if cross-origin requests lead to valid responses
317 /// </summary>
318 public enum RequestMode
320 /// <summary>
321 /// If a request is made to another origin with this mode set, the result is simply an error
322 /// </summary>
323 [Export("same-origin")]
324 SameOrigin,
326 /// <summary>
327 /// Prevents the method from being anything other than HEAD, GET or POST, and the headers from
328 /// being anything other than simple headers.
329 /// </summary>
330 [Export("no-cors")]
331 NoCors,
333 /// <summary>
334 /// Allows cross-origin requests, for example to access various APIs offered by 3rd party vendors.
335 /// </summary>
336 [Export(EnumValue = ConvertEnum.ToLower)]
337 Cors,
339 /// <summary>
340 /// A mode for supporting navigation.
341 /// </summary>
342 [Export(EnumValue = ConvertEnum.ToLower)]
343 Navigate,