Skip to content

Commit ad3b340

Browse files
github-actions[bot]pavelsavaracampersauakoeplinger
authored
[release/9.0] [browser][http] mute JS exceptions about network errors + HEAD verb (#113261)
Co-authored-by: pavelsavara <pavel.savara@gmail.com> Co-authored-by: campersau <buchholz.bastian@googlemail.com> Co-authored-by: Alexander Köplinger <alex.koeplinger@outlook.com>
1 parent f5bc37b commit ad3b340

File tree

3 files changed

+162
-36
lines changed

3 files changed

+162
-36
lines changed

src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,99 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders
230230

231231
#if NET
232232

233+
public static IEnumerable<object[]> HttpMethods => new object[][]
234+
{
235+
new [] { HttpMethod.Get },
236+
new [] { HttpMethod.Head },
237+
new [] { HttpMethod.Post },
238+
new [] { HttpMethod.Put },
239+
new [] { HttpMethod.Delete },
240+
new [] { HttpMethod.Options },
241+
new [] { HttpMethod.Patch },
242+
};
243+
244+
public static IEnumerable<object[]> HttpMethodsAndAbort => new object[][]
245+
{
246+
new object[] { HttpMethod.Get, "abortBeforeHeaders" },
247+
new object[] { HttpMethod.Head , "abortBeforeHeaders"},
248+
new object[] { HttpMethod.Post , "abortBeforeHeaders"},
249+
new object[] { HttpMethod.Put , "abortBeforeHeaders"},
250+
new object[] { HttpMethod.Delete , "abortBeforeHeaders"},
251+
new object[] { HttpMethod.Options , "abortBeforeHeaders"},
252+
new object[] { HttpMethod.Patch , "abortBeforeHeaders"},
253+
254+
new object[] { HttpMethod.Get, "abortAfterHeaders" },
255+
new object[] { HttpMethod.Post , "abortAfterHeaders"},
256+
new object[] { HttpMethod.Put , "abortAfterHeaders"},
257+
new object[] { HttpMethod.Delete , "abortAfterHeaders"},
258+
new object[] { HttpMethod.Options , "abortAfterHeaders"},
259+
new object[] { HttpMethod.Patch , "abortAfterHeaders"},
260+
261+
new object[] { HttpMethod.Get, "abortDuringBody" },
262+
new object[] { HttpMethod.Post , "abortDuringBody"},
263+
new object[] { HttpMethod.Put , "abortDuringBody"},
264+
new object[] { HttpMethod.Delete , "abortDuringBody"},
265+
new object[] { HttpMethod.Options , "abortDuringBody"},
266+
new object[] { HttpMethod.Patch , "abortDuringBody"},
267+
268+
};
269+
270+
[MemberData(nameof(HttpMethods))]
271+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))]
272+
public async Task BrowserHttpHandler_StreamingResponse(HttpMethod method)
273+
{
274+
var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
275+
276+
var req = new HttpRequestMessage(method, Configuration.Http.RemoteHttp11Server.BaseUri + "echo.ashx");
277+
req.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
278+
279+
if (method == HttpMethod.Post)
280+
{
281+
req.Content = new StringContent("hello world");
282+
}
283+
284+
using (HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteHttp11Server))
285+
// we need to switch off Response buffering of default ResponseContentRead option
286+
using (HttpResponseMessage response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
287+
{
288+
using var content = response.Content;
289+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
290+
Assert.Equal(typeof(StreamContent), content.GetType());
291+
Assert.NotEqual(0, content.Headers.ContentLength);
292+
if (method != HttpMethod.Head)
293+
{
294+
var data = await content.ReadAsByteArrayAsync();
295+
Assert.NotEqual(0, data.Length);
296+
}
297+
}
298+
}
299+
300+
[MemberData(nameof(HttpMethodsAndAbort))]
301+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))]
302+
public async Task BrowserHttpHandler_StreamingResponseAbort(HttpMethod method, string abort)
303+
{
304+
var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
305+
306+
var req = new HttpRequestMessage(method, Configuration.Http.RemoteHttp11Server.BaseUri + "echo.ashx?" + abort + "=true");
307+
req.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
308+
309+
if (method == HttpMethod.Post || method == HttpMethod.Put || method == HttpMethod.Patch)
310+
{
311+
req.Content = new StringContent("hello world");
312+
}
313+
314+
using HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteHttp11Server);
315+
if (abort == "abortDuringBody")
316+
{
317+
using var res = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
318+
await Assert.ThrowsAsync<HttpRequestException>(() => res.Content.ReadAsByteArrayAsync());
319+
}
320+
else
321+
{
322+
await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead));
323+
}
324+
}
325+
233326
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
234327
public async Task BrowserHttpHandler_Streaming()
235328
{
@@ -486,7 +579,7 @@ public async Task BrowserHttpHandler_StreamingRequest_Http1Fails()
486579

487580
[OuterLoop]
488581
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
489-
public async Task BrowserHttpHandler_StreamingResponse()
582+
public async Task BrowserHttpHandler_StreamingResponseLarge()
490583
{
491584
var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
492585

src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,37 @@ public static async Task InvokeAsync(HttpContext context)
2222
return;
2323
}
2424

25-
// Add original request method verb as a custom response header.
26-
context.Response.Headers["X-HttpRequest-Method"] = context.Request.Method;
27-
28-
// Echo back JSON encoded payload.
29-
RequestInformation info = await RequestInformation.CreateAsync(context.Request);
30-
string echoJson = info.SerializeToJson();
31-
32-
byte[] bytes = Encoding.UTF8.GetBytes(echoJson);
3325

26+
var qs = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : "";
3427
var delay = 0;
35-
if (context.Request.QueryString.HasValue)
28+
if (qs.Contains("delay1sec"))
3629
{
37-
if (context.Request.QueryString.Value.Contains("delay1sec"))
38-
{
39-
delay = 1000;
40-
}
41-
else if (context.Request.QueryString.Value.Contains("delay10sec"))
42-
{
43-
delay = 10000;
44-
}
30+
delay = 1000;
31+
}
32+
else if (qs.Contains("delay10sec"))
33+
{
34+
delay = 10000;
35+
}
36+
37+
if (qs.Contains("abortBeforeHeaders"))
38+
{
39+
context.Abort();
40+
return;
4541
}
4642

4743
if (delay > 0)
4844
{
4945
context.Features.Get<IHttpResponseBodyFeature>().DisableBuffering();
5046
}
5147

48+
// Echo back JSON encoded payload.
49+
RequestInformation info = await RequestInformation.CreateAsync(context.Request);
50+
string echoJson = info.SerializeToJson();
51+
byte[] bytes = Encoding.UTF8.GetBytes(echoJson);
52+
53+
// Add original request method verb as a custom response header.
54+
context.Response.Headers["X-HttpRequest-Method"] = context.Request.Method;
55+
5256
// Compute MD5 hash so that clients can verify the received data.
5357
using (MD5 md5 = MD5.Create())
5458
{
@@ -60,11 +64,32 @@ public static async Task InvokeAsync(HttpContext context)
6064
context.Response.ContentLength = bytes.Length;
6165
}
6266

63-
if (delay > 0)
67+
await context.Response.StartAsync(CancellationToken.None);
68+
69+
if (qs.Contains("abortAfterHeaders"))
70+
{
71+
await Task.Delay(10);
72+
context.Abort();
73+
return;
74+
}
75+
76+
if (HttpMethods.IsHead(context.Request.Method))
77+
{
78+
return;
79+
}
80+
81+
if (delay > 0 || qs.Contains("abortDuringBody"))
6482
{
65-
await context.Response.StartAsync(CancellationToken.None);
6683
await context.Response.Body.WriteAsync(bytes, 0, 10);
6784
await context.Response.Body.FlushAsync();
85+
if (qs.Contains("abortDuringBody"))
86+
{
87+
await context.Response.Body.FlushAsync();
88+
await Task.Delay(10);
89+
context.Abort();
90+
return;
91+
}
92+
6893
await Task.Delay(delay);
6994
await context.Response.Body.WriteAsync(bytes, 10, bytes.Length-10);
7095
await context.Response.Body.FlushAsync();

src/mono/browser/runtime/http.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
import BuildConfiguration from "consts:configuration";
55

66
import { wrap_as_cancelable_promise } from "./cancelable-promise";
7-
import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert } from "./globals";
7+
import { ENVIRONMENT_IS_NODE, loaderHelpers, mono_assert } from "./globals";
88
import { assert_js_interop } from "./invoke-js";
99
import { MemoryViewType, Span } from "./marshal";
1010
import type { VoidPtr } from "./types/emscripten";
1111
import { ControllablePromise } from "./types/internal";
12+
import { mono_log_debug } from "./logging";
1213

1314

1415
function verifyEnvironment () {
@@ -72,12 +73,11 @@ export function http_wasm_create_controller (): HttpController {
7273
return controller;
7374
}
7475

75-
function handle_abort_error (promise:Promise<any>) {
76+
function mute_unhandledrejection (promise:Promise<any>) {
7677
promise.catch((err) => {
7778
if (err && err !== "AbortError" && err.name !== "AbortError" ) {
78-
Module.err("Unexpected error: " + err);
79+
mono_log_debug("http muted: " + err);
7980
}
80-
// otherwise, it's expected
8181
});
8282
}
8383

@@ -86,15 +86,15 @@ export function http_wasm_abort (controller: HttpController): void {
8686
try {
8787
if (!controller.isAborted) {
8888
if (controller.streamWriter) {
89-
handle_abort_error(controller.streamWriter.abort());
89+
mute_unhandledrejection(controller.streamWriter.abort());
9090
controller.isAborted = true;
9191
}
9292
if (controller.streamReader) {
93-
handle_abort_error(controller.streamReader.cancel());
93+
mute_unhandledrejection(controller.streamReader.cancel());
9494
controller.isAborted = true;
9595
}
9696
}
97-
if (!controller.isAborted) {
97+
if (!controller.isAborted && !controller.abortController.signal.aborted) {
9898
controller.abortController.abort("AbortError");
9999
}
100100
} catch (err) {
@@ -138,8 +138,8 @@ export function http_wasm_fetch_stream (controller: HttpController, url: string,
138138
if (BuildConfiguration === "Debug") commonAsserts(controller);
139139
const transformStream = new TransformStream<Uint8Array, Uint8Array>();
140140
controller.streamWriter = transformStream.writable.getWriter();
141-
handle_abort_error(controller.streamWriter.closed);
142-
handle_abort_error(controller.streamWriter.ready);
141+
mute_unhandledrejection(controller.streamWriter.closed);
142+
mute_unhandledrejection(controller.streamWriter.ready);
143143
const fetch_promise = http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, transformStream.readable);
144144
return fetch_promise;
145145
}
@@ -177,16 +177,18 @@ export function http_wasm_fetch (controller: HttpController, url: string, header
177177
}
178178
// make the fetch cancellable
179179
controller.responsePromise = wrap_as_cancelable_promise(() => {
180-
return loaderHelpers.fetch_like(url, options);
180+
return loaderHelpers.fetch_like(url, options).then((res: Response) => {
181+
controller.response = res;
182+
return null;// drop the response from the promise chain
183+
});
181184
});
182185
// avoid processing headers if the fetch is canceled
183-
controller.responsePromise.then((res: Response) => {
184-
controller.response = res;
186+
controller.responsePromise.then(() => {
187+
mono_assert(controller.response, "expected response");
185188
controller.responseHeaderNames = [];
186189
controller.responseHeaderValues = [];
187-
if (res.headers && (<any>res.headers).entries) {
188-
const entries: Iterable<string[]> = (<any>res.headers).entries();
189-
190+
if (controller.response.headers && (<any>controller.response.headers).entries) {
191+
const entries: Iterable<string[]> = (<any>controller.response.headers).entries();
190192
for (const pair of entries) {
191193
controller.responseHeaderNames.push(pair[0]);
192194
controller.responseHeaderValues.push(pair[1]);
@@ -250,9 +252,15 @@ export function http_wasm_get_streamed_response_bytes (controller: HttpControlle
250252
// the bufferPtr is pinned by the caller
251253
const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte);
252254
return wrap_as_cancelable_promise(async () => {
255+
await controller.responsePromise;
253256
mono_assert(controller.response, "expected response");
257+
if (!controller.response.body) {
258+
// in FF when the verb is HEAD, the body is null
259+
return 0;
260+
}
254261
if (!controller.streamReader) {
255-
controller.streamReader = controller.response.body!.getReader();
262+
controller.streamReader = controller.response.body.getReader();
263+
mute_unhandledrejection(controller.streamReader.closed);
256264
}
257265
if (!controller.currentStreamReaderChunk || controller.currentBufferOffset === undefined) {
258266
controller.currentStreamReaderChunk = await controller.streamReader.read();

0 commit comments

Comments
 (0)