Skip to content

Commit 5a1fc27

Browse files
authored
[release/8.0][browser][http] mute JS exceptions about network errors + HEAD verb (#113271)
1 parent 13eabb4 commit 5a1fc27

File tree

3 files changed

+171
-11
lines changed

3 files changed

+171
-11
lines changed

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,99 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders
229229
}
230230

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

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

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
using System;
55
using System.Security.Cryptography;
66
using System.Text;
7+
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Http;
10+
using Microsoft.AspNetCore.Http.Features;
911

1012
namespace NetCoreServer
1113
{
@@ -20,23 +22,80 @@ public static async Task InvokeAsync(HttpContext context)
2022
return;
2123
}
2224

23-
// Add original request method verb as a custom response header.
24-
context.Response.Headers["X-HttpRequest-Method"] = context.Request.Method;
25+
26+
var qs = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : "";
27+
var delay = 0;
28+
if (qs.Contains("delay1sec"))
29+
{
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;
41+
}
42+
43+
if (delay > 0)
44+
{
45+
context.Features.Get<IHttpResponseBodyFeature>().DisableBuffering();
46+
}
2547

2648
// Echo back JSON encoded payload.
2749
RequestInformation info = await RequestInformation.CreateAsync(context.Request);
2850
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;
2955

3056
// Compute MD5 hash so that clients can verify the received data.
3157
using (MD5 md5 = MD5.Create())
3258
{
33-
byte[] bytes = Encoding.UTF8.GetBytes(echoJson);
3459
byte[] hash = md5.ComputeHash(bytes);
3560
string encodedHash = Convert.ToBase64String(hash);
3661

3762
context.Response.Headers["Content-MD5"] = encodedHash;
3863
context.Response.ContentType = "application/json";
3964
context.Response.ContentLength = bytes.Length;
65+
}
66+
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(context.Request.Method == "HEAD")
77+
{
78+
return;
79+
}
80+
81+
if (delay > 0 || qs.Contains("abortDuringBody"))
82+
{
83+
await context.Response.Body.WriteAsync(bytes, 0, 10);
84+
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+
93+
await Task.Delay(delay);
94+
await context.Response.Body.WriteAsync(bytes, 10, bytes.Length-10);
95+
await context.Response.Body.FlushAsync();
96+
}
97+
else
98+
{
4099
await context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
41100
}
42101
}

src/mono/wasm/runtime/http.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
import { wrap_as_cancelable_promise } from "./cancelable-promise";
5-
import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert } from "./globals";
5+
import { ENVIRONMENT_IS_NODE, loaderHelpers, mono_assert } from "./globals";
6+
import { mono_log_debug } from "./logging";
67
import { MemoryViewType, Span } from "./marshal";
78
import type { VoidPtr } from "./types/emscripten";
89

@@ -16,6 +17,14 @@ function verifyEnvironment() {
1617
}
1718
}
1819

20+
function mute_unhandledrejection (promise:Promise<any>) {
21+
promise.catch((err) => {
22+
if (err && err !== "AbortError" && err.name !== "AbortError" ) {
23+
mono_log_debug("http muted: " + err);
24+
}
25+
});
26+
}
27+
1928
export function http_wasm_supports_streaming_response(): boolean {
2029
return typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function";
2130
}
@@ -32,12 +41,7 @@ export function http_wasm_abort_request(abort_controller: AbortController): void
3241
export function http_wasm_abort_response(res: ResponseExtension): void {
3342
res.__abort_controller.abort();
3443
if (res.__reader) {
35-
res.__reader.cancel().catch((err) => {
36-
if (err && err.name !== "AbortError") {
37-
Module.err("Error in http_wasm_abort_response: " + err);
38-
}
39-
// otherwise, it's expected
40-
});
44+
mute_unhandledrejection(res.__reader.cancel());
4145
}
4246
}
4347

@@ -123,8 +127,12 @@ export function http_wasm_get_streamed_response_bytes(res: ResponseExtension, bu
123127
// the bufferPtr is pinned by the caller
124128
const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte);
125129
return wrap_as_cancelable_promise(async () => {
130+
if (!res.body) {
131+
return 0;
132+
}
126133
if (!res.__reader) {
127-
res.__reader = res.body!.getReader();
134+
res.__reader = res.body.getReader();
135+
mute_unhandledrejection(res.__reader.closed);
128136
}
129137
if (!res.__chunk) {
130138
res.__chunk = await res.__reader.read();

0 commit comments

Comments
 (0)