diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index 992851b166befa..77940f5fa32af0 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -229,6 +229,99 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders } #if NETCOREAPP + public static IEnumerable HttpMethods => new object[][] + { + new [] { HttpMethod.Get }, + new [] { HttpMethod.Head }, + new [] { HttpMethod.Post }, + new [] { HttpMethod.Put }, + new [] { HttpMethod.Delete }, + new [] { HttpMethod.Options }, + new [] { HttpMethod.Patch }, + }; + + public static IEnumerable HttpMethodsAndAbort => new object[][] + { + new object[] { HttpMethod.Get, "abortBeforeHeaders" }, + new object[] { HttpMethod.Head , "abortBeforeHeaders"}, + new object[] { HttpMethod.Post , "abortBeforeHeaders"}, + new object[] { HttpMethod.Put , "abortBeforeHeaders"}, + new object[] { HttpMethod.Delete , "abortBeforeHeaders"}, + new object[] { HttpMethod.Options , "abortBeforeHeaders"}, + new object[] { HttpMethod.Patch , "abortBeforeHeaders"}, + + new object[] { HttpMethod.Get, "abortAfterHeaders" }, + new object[] { HttpMethod.Post , "abortAfterHeaders"}, + new object[] { HttpMethod.Put , "abortAfterHeaders"}, + new object[] { HttpMethod.Delete , "abortAfterHeaders"}, + new object[] { HttpMethod.Options , "abortAfterHeaders"}, + new object[] { HttpMethod.Patch , "abortAfterHeaders"}, + + new object[] { HttpMethod.Get, "abortDuringBody" }, + new object[] { HttpMethod.Post , "abortDuringBody"}, + new object[] { HttpMethod.Put , "abortDuringBody"}, + new object[] { HttpMethod.Delete , "abortDuringBody"}, + new object[] { HttpMethod.Options , "abortDuringBody"}, + new object[] { HttpMethod.Patch , "abortDuringBody"}, + + }; + + [MemberData(nameof(HttpMethods))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] + public async Task BrowserHttpHandler_StreamingResponse(HttpMethod method) + { + var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); + + var req = new HttpRequestMessage(method, Configuration.Http.RemoteHttp11Server.BaseUri + "echo.ashx"); + req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); + + if(method == HttpMethod.Post) + { + req.Content = new StringContent("hello world"); + } + + using (HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteHttp11Server)) + // we need to switch off Response buffering of default ResponseContentRead option + using (HttpResponseMessage response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)) + { + using var content = response.Content; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(typeof(StreamContent), content.GetType()); + Assert.NotEqual(0, content.Headers.ContentLength); + if (method != HttpMethod.Head) + { + var data = await content.ReadAsByteArrayAsync(); + Assert.NotEqual(0, data.Length); + } + } + } + + [MemberData(nameof(HttpMethodsAndAbort))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] + public async Task BrowserHttpHandler_StreamingResponseAbort(HttpMethod method, string abort) + { + var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); + + var req = new HttpRequestMessage(method, Configuration.Http.RemoteHttp11Server.BaseUri + "echo.ashx?" + abort + "=true"); + req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); + + if (method == HttpMethod.Post || method == HttpMethod.Put || method == HttpMethod.Patch) + { + req.Content = new StringContent("hello world"); + } + + HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteHttp11Server); + if (abort == "abortDuringBody") + { + using var res = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + await Assert.ThrowsAsync(() => res.Content.ReadAsByteArrayAsync()); + } + else + { + await Assert.ThrowsAsync(() => client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)); + } + } + [OuterLoop] [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] public async Task BrowserHttpHandler_Streaming() diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs index fd05cff102d2e6..690e759a90e8b5 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs @@ -4,8 +4,10 @@ using System; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; namespace NetCoreServer { @@ -20,23 +22,80 @@ public static async Task InvokeAsync(HttpContext context) return; } - // Add original request method verb as a custom response header. - context.Response.Headers["X-HttpRequest-Method"] = context.Request.Method; + + var qs = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; + var delay = 0; + if (qs.Contains("delay1sec")) + { + delay = 1000; + } + else if (qs.Contains("delay10sec")) + { + delay = 10000; + } + + if (qs.Contains("abortBeforeHeaders")) + { + context.Abort(); + return; + } + + if (delay > 0) + { + context.Features.Get().DisableBuffering(); + } // Echo back JSON encoded payload. RequestInformation info = await RequestInformation.CreateAsync(context.Request); string echoJson = info.SerializeToJson(); + byte[] bytes = Encoding.UTF8.GetBytes(echoJson); + + // Add original request method verb as a custom response header. + context.Response.Headers["X-HttpRequest-Method"] = context.Request.Method; // Compute MD5 hash so that clients can verify the received data. using (MD5 md5 = MD5.Create()) { - byte[] bytes = Encoding.UTF8.GetBytes(echoJson); byte[] hash = md5.ComputeHash(bytes); string encodedHash = Convert.ToBase64String(hash); context.Response.Headers["Content-MD5"] = encodedHash; context.Response.ContentType = "application/json"; context.Response.ContentLength = bytes.Length; + } + + await context.Response.StartAsync(CancellationToken.None); + + if (qs.Contains("abortAfterHeaders")) + { + await Task.Delay(10); + context.Abort(); + return; + } + + if(context.Request.Method == "HEAD") + { + return; + } + + if (delay > 0 || qs.Contains("abortDuringBody")) + { + await context.Response.Body.WriteAsync(bytes, 0, 10); + await context.Response.Body.FlushAsync(); + if (qs.Contains("abortDuringBody")) + { + await context.Response.Body.FlushAsync(); + await Task.Delay(10); + context.Abort(); + return; + } + + await Task.Delay(delay); + await context.Response.Body.WriteAsync(bytes, 10, bytes.Length-10); + await context.Response.Body.FlushAsync(); + } + else + { await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); } } diff --git a/src/mono/wasm/runtime/http.ts b/src/mono/wasm/runtime/http.ts index 1ad6a4fc457bbd..c1949ce0d589eb 100644 --- a/src/mono/wasm/runtime/http.ts +++ b/src/mono/wasm/runtime/http.ts @@ -2,7 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import { wrap_as_cancelable_promise } from "./cancelable-promise"; -import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert } from "./globals"; +import { ENVIRONMENT_IS_NODE, loaderHelpers, mono_assert } from "./globals"; +import { mono_log_debug } from "./logging"; import { MemoryViewType, Span } from "./marshal"; import type { VoidPtr } from "./types/emscripten"; @@ -16,6 +17,14 @@ function verifyEnvironment() { } } +function mute_unhandledrejection (promise:Promise) { + promise.catch((err) => { + if (err && err !== "AbortError" && err.name !== "AbortError" ) { + mono_log_debug("http muted: " + err); + } + }); +} + export function http_wasm_supports_streaming_response(): boolean { return typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function"; } @@ -32,12 +41,7 @@ export function http_wasm_abort_request(abort_controller: AbortController): void export function http_wasm_abort_response(res: ResponseExtension): void { res.__abort_controller.abort(); if (res.__reader) { - res.__reader.cancel().catch((err) => { - if (err && err.name !== "AbortError") { - Module.err("Error in http_wasm_abort_response: " + err); - } - // otherwise, it's expected - }); + mute_unhandledrejection(res.__reader.cancel()); } } @@ -123,8 +127,12 @@ export function http_wasm_get_streamed_response_bytes(res: ResponseExtension, bu // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrap_as_cancelable_promise(async () => { + if (!res.body) { + return 0; + } if (!res.__reader) { - res.__reader = res.body!.getReader(); + res.__reader = res.body.getReader(); + mute_unhandledrejection(res.__reader.closed); } if (!res.__chunk) { res.__chunk = await res.__reader.read();