Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,99 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders
}

#if NETCOREAPP
public static IEnumerable<object[]> 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<object[]> 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<bool>("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<bool>("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<HttpRequestException>(() => res.Content.ReadAsByteArrayAsync());
}
else
{
await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead));
}
}

[OuterLoop]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))]
public async Task BrowserHttpHandler_Streaming()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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<IHttpResponseBodyFeature>().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);
}
}
Expand Down
24 changes: 16 additions & 8 deletions src/mono/wasm/runtime/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -16,6 +17,14 @@ function verifyEnvironment() {
}
}

function mute_unhandledrejection (promise:Promise<any>) {
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";
}
Expand All @@ -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());
Comment thread
pavelsavara marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -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();
Expand Down