diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index 7f58fd5b2424e8..61958c04a26b0c 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -230,6 +230,99 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders #if NET + 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"); + } + + using 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)); + } + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))] public async Task BrowserHttpHandler_Streaming() { @@ -486,7 +579,7 @@ public async Task BrowserHttpHandler_StreamingRequest_Http1Fails() [OuterLoop] [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))] - public async Task BrowserHttpHandler_StreamingResponse() + public async Task BrowserHttpHandler_StreamingResponseLarge() { var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); 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 667e99c29dc398..f9f8b0c24f2e01 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 @@ -22,26 +22,22 @@ 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; - - // Echo back JSON encoded payload. - RequestInformation info = await RequestInformation.CreateAsync(context.Request); - string echoJson = info.SerializeToJson(); - - byte[] bytes = Encoding.UTF8.GetBytes(echoJson); + var qs = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; var delay = 0; - if (context.Request.QueryString.HasValue) + if (qs.Contains("delay1sec")) { - if (context.Request.QueryString.Value.Contains("delay1sec")) - { - delay = 1000; - } - else if (context.Request.QueryString.Value.Contains("delay10sec")) - { - delay = 10000; - } + delay = 1000; + } + else if (qs.Contains("delay10sec")) + { + delay = 10000; + } + + if (qs.Contains("abortBeforeHeaders")) + { + context.Abort(); + return; } if (delay > 0) @@ -49,6 +45,14 @@ public static async Task InvokeAsync(HttpContext context) 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()) { @@ -60,11 +64,32 @@ public static async Task InvokeAsync(HttpContext context) context.Response.ContentLength = bytes.Length; } - if (delay > 0) + await context.Response.StartAsync(CancellationToken.None); + + if (qs.Contains("abortAfterHeaders")) + { + await Task.Delay(10); + context.Abort(); + return; + } + + if (HttpMethods.IsHead(context.Request.Method)) + { + return; + } + + if (delay > 0 || qs.Contains("abortDuringBody")) { - await context.Response.StartAsync(CancellationToken.None); 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(); diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index 743972efd8df71..74f86f88791c4d 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -4,11 +4,12 @@ import BuildConfiguration from "consts:configuration"; 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 { assert_js_interop } from "./invoke-js"; import { MemoryViewType, Span } from "./marshal"; import type { VoidPtr } from "./types/emscripten"; import { ControllablePromise } from "./types/internal"; +import { mono_log_debug } from "./logging"; function verifyEnvironment () { @@ -72,12 +73,11 @@ export function http_wasm_create_controller (): HttpController { return controller; } -function handle_abort_error (promise:Promise) { +function mute_unhandledrejection (promise:Promise) { promise.catch((err) => { if (err && err !== "AbortError" && err.name !== "AbortError" ) { - Module.err("Unexpected error: " + err); + mono_log_debug("http muted: " + err); } - // otherwise, it's expected }); } @@ -86,15 +86,15 @@ export function http_wasm_abort (controller: HttpController): void { try { if (!controller.isAborted) { if (controller.streamWriter) { - handle_abort_error(controller.streamWriter.abort()); + mute_unhandledrejection(controller.streamWriter.abort()); controller.isAborted = true; } if (controller.streamReader) { - handle_abort_error(controller.streamReader.cancel()); + mute_unhandledrejection(controller.streamReader.cancel()); controller.isAborted = true; } } - if (!controller.isAborted) { + if (!controller.isAborted && !controller.abortController.signal.aborted) { controller.abortController.abort("AbortError"); } } catch (err) { @@ -138,8 +138,8 @@ export function http_wasm_fetch_stream (controller: HttpController, url: string, if (BuildConfiguration === "Debug") commonAsserts(controller); const transformStream = new TransformStream(); controller.streamWriter = transformStream.writable.getWriter(); - handle_abort_error(controller.streamWriter.closed); - handle_abort_error(controller.streamWriter.ready); + mute_unhandledrejection(controller.streamWriter.closed); + mute_unhandledrejection(controller.streamWriter.ready); const fetch_promise = http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, transformStream.readable); return fetch_promise; } @@ -177,16 +177,18 @@ export function http_wasm_fetch (controller: HttpController, url: string, header } // make the fetch cancellable controller.responsePromise = wrap_as_cancelable_promise(() => { - return loaderHelpers.fetch_like(url, options); + return loaderHelpers.fetch_like(url, options).then((res: Response) => { + controller.response = res; + return null;// drop the response from the promise chain + }); }); // avoid processing headers if the fetch is canceled - controller.responsePromise.then((res: Response) => { - controller.response = res; + controller.responsePromise.then(() => { + mono_assert(controller.response, "expected response"); controller.responseHeaderNames = []; controller.responseHeaderValues = []; - if (res.headers && (res.headers).entries) { - const entries: Iterable = (res.headers).entries(); - + if (controller.response.headers && (controller.response.headers).entries) { + const entries: Iterable = (controller.response.headers).entries(); for (const pair of entries) { controller.responseHeaderNames.push(pair[0]); controller.responseHeaderValues.push(pair[1]); @@ -250,9 +252,15 @@ export function http_wasm_get_streamed_response_bytes (controller: HttpControlle // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrap_as_cancelable_promise(async () => { + await controller.responsePromise; mono_assert(controller.response, "expected response"); + if (!controller.response.body) { + // in FF when the verb is HEAD, the body is null + return 0; + } if (!controller.streamReader) { - controller.streamReader = controller.response.body!.getReader(); + controller.streamReader = controller.response.body.getReader(); + mute_unhandledrejection(controller.streamReader.closed); } if (!controller.currentStreamReaderChunk || controller.currentBufferOffset === undefined) { controller.currentStreamReaderChunk = await controller.streamReader.read();