2 using System
.Collections
.Generic
;
6 using System
.Threading
;
7 using System
.Threading
.Tasks
;
9 namespace WebAssembly
.Net
.Http
.HttpClient
11 public class WasmHttpMessageHandler
: HttpMessageHandler
15 static JSObject fetch
;
16 static JSObject window
;
17 static JSObject
global;
20 /// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
21 /// Defaults to <see cref="FetchCredentialsOption.SameOrigin"/>.
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; }
32 public WasmHttpMessageHandler()
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.
45 BINDING.mono_wasm_get_global()[""__mono_wasm_headers_hook__""] = function () { return new Headers(); }
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
)
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());
93 requestObject
.SetObjectProperty("body", await request
.Content
.ReadAsByteArrayAsync());
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
);
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()));
141 httpresponse
.Content
= new ByteArrayContent(buffer
);
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
);
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.
218 // Suppress finalization.
219 GC
.SuppressFinalize(this);
222 // Protected implementation of Dispose pattern.
223 protected virtual void Dispose(bool disposing
)
229 // Free any other managed objects here.
233 // Free any unmanaged objects here.
235 managedJSObject
?.Dispose();
236 managedJSObject
= null;
244 /// Specifies a value for the 'credentials' option on outbound HTTP requests.
246 public enum FetchCredentialsOption
249 /// Advises the browser never to send credentials (such as cookies or HTTP auth headers).
251 [Export(EnumValue
= ConvertEnum
.ToLower
)]
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.
258 [Export("same-origin")]
262 /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
263 /// even for cross-origin requests.
265 [Export(EnumValue
= ConvertEnum
.ToLower
)]
271 /// The cache mode of the request. It controls how the request will interact with the browser's HTTP cache.
273 public enum RequestCache
276 /// The browser looks for a matching request in its HTTP cache.
278 [Export(EnumValue
= ConvertEnum
.ToLower
)]
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.
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.
292 [Export(EnumValue
= ConvertEnum
.ToLower
)]
296 /// The browser looks for a matching request in its HTTP cache.
302 /// The browser looks for a matching request in its HTTP cache.
304 [Export("force-cache")]
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"
311 [Export("only-if-cached")]
316 /// The mode of the request. This is used to determine if cross-origin requests lead to valid responses
318 public enum RequestMode
321 /// If a request is made to another origin with this mode set, the result is simply an error
323 [Export("same-origin")]
327 /// Prevents the method from being anything other than HEAD, GET or POST, and the headers from
328 /// being anything other than simple headers.
334 /// Allows cross-origin requests, for example to access various APIs offered by 3rd party vendors.
336 [Export(EnumValue
= ConvertEnum
.ToLower
)]
340 /// A mode for supporting navigation.
342 [Export(EnumValue
= ConvertEnum
.ToLower
)]