Skip to content

Commit edbb2ba

Browse files
authored
[browser] http streaming request server error (#105709)
* wip * more
1 parent 6b558d9 commit edbb2ba

File tree

9 files changed

+139
-60
lines changed

9 files changed

+139
-60
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public static partial class Http
5151
private const string EmptyContentHandler = "EmptyContent.ashx";
5252
private const string RedirectHandler = "Redirect.ashx";
5353
private const string VerifyUploadHandler = "VerifyUpload.ashx";
54+
private const string StatusCodeHandler = "StatusCode.ashx";
5455
private const string DeflateHandler = "Deflate.ashx";
5556
private const string GZipHandler = "GZip.ashx";
5657
private const string RemoteLoopHandler = "RemoteLoop";
@@ -71,6 +72,7 @@ public static Uri[] GetEchoServerList()
7172
public static readonly Uri RemoteVerifyUploadServer = new Uri("http://" + Host + "/" + VerifyUploadHandler);
7273
public static readonly Uri SecureRemoteVerifyUploadServer = new Uri("https://" + SecureHost + "/" + VerifyUploadHandler);
7374
public static readonly Uri Http2RemoteVerifyUploadServer = new Uri("https://" + Http2Host + "/" + VerifyUploadHandler);
75+
public static readonly Uri Http2RemoteStatusCodeServer = new Uri("https://" + Http2Host + "/" + StatusCodeHandler);
7476

7577
public static readonly Uri RemoteEmptyContentServer = new Uri("http://" + Host + "/" + EmptyContentHandler);
7678
public static readonly Uri RemoteDeflateServer = new Uri("http://" + Host + "/" + DeflateHandler);

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

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ public async Task BrowserHttpHandler_Streaming()
286286
}
287287
}
288288

289-
[OuterLoop]
290289
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
291290
public async Task BrowserHttpHandler_StreamingRequest()
292291
{
@@ -328,8 +327,44 @@ public async Task BrowserHttpHandler_StreamingRequest()
328327
}
329328
}
330329

330+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
331+
public async Task BrowserHttpHandler_StreamingRequest_ServerFail()
332+
{
333+
var WebAssemblyEnableStreamingRequestKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingRequest");
334+
335+
var requestUrl = new UriBuilder(Configuration.Http.Http2RemoteStatusCodeServer) { Query = "statuscode=500&statusdescription=test&delay=100" };
336+
var req = new HttpRequestMessage(HttpMethod.Post, requestUrl.Uri);
337+
338+
req.Options.Set(WebAssemblyEnableStreamingRequestKey, true);
339+
340+
int size = 1500 * 1024 * 1024;
341+
int remaining = size;
342+
var content = new MultipartFormDataContent();
343+
content.Add(new StreamContent(new DelegateStream(
344+
canReadFunc: () => true,
345+
readFunc: (buffer, offset, count) => throw new FormatException(),
346+
readAsyncFunc: (buffer, offset, count, cancellationToken) =>
347+
{
348+
if (remaining > 0)
349+
{
350+
int send = Math.Min(remaining, count);
351+
buffer.AsSpan(offset, send).Fill(65);
352+
remaining -= send;
353+
return Task.FromResult(send);
354+
}
355+
return Task.FromResult(0);
356+
})), "test");
357+
req.Content = content;
358+
359+
req.Content.Headers.Add("Content-MD5-Skip", "browser");
360+
361+
using HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteHttp2Server);
362+
using HttpResponseMessage response = await client.SendAsync(req);
363+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
364+
}
365+
366+
331367
// Duplicate of PostAsync_ThrowFromContentCopy_RequestFails using remote server
332-
[OuterLoop]
333368
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
334369
[InlineData(false)]
335370
[InlineData(true)]
@@ -357,20 +392,19 @@ public async Task BrowserHttpHandler_StreamingRequest_ThrowFromContentCopy_Reque
357392
}
358393

359394
public static TheoryData CancelRequestReadFunctions
360-
=> new TheoryData<bool, Func<Task<int>>>
395+
=> new TheoryData<bool, int, bool>
361396
{
362-
{ false, () => Task.FromResult(0) },
363-
{ true, () => Task.FromResult(0) },
364-
{ false, () => Task.FromResult(1) },
365-
{ true, () => Task.FromResult(1) },
366-
{ false, () => throw new FormatException() },
367-
{ true, () => throw new FormatException() },
397+
{ false, 0, false },
398+
{ true, 0, false },
399+
{ false, 1, false },
400+
{ true, 1, false },
401+
{ false, 0, true },
402+
{ true, 0, true },
368403
};
369404

370-
[OuterLoop]
371405
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
372406
[MemberData(nameof(CancelRequestReadFunctions))]
373-
public async Task BrowserHttpHandler_StreamingRequest_CancelRequest(bool cancelAsync, Func<Task<int>> readFunc)
407+
public async Task BrowserHttpHandler_StreamingRequest_CancelRequest(bool cancelAsync, int bytes, bool throwException)
374408
{
375409
var WebAssemblyEnableStreamingRequestKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingRequest");
376410

@@ -397,13 +431,26 @@ public async Task BrowserHttpHandler_StreamingRequest_CancelRequest(bool cancelA
397431
{
398432
readCancelledCount++;
399433
}
400-
return await readFunc();
434+
if (throwException)
435+
{
436+
throw new FormatException("Test");
437+
}
438+
return await Task.FromResult(bytes);
401439
}));
402440

403441
using (HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteHttp2Server))
404442
{
405-
TaskCanceledException ex = await Assert.ThrowsAsync<TaskCanceledException>(() => client.SendAsync(req, token));
406-
Assert.Equal(token, ex.CancellationToken);
443+
Exception ex = await Assert.ThrowsAnyAsync<Exception>(() => client.SendAsync(req, token));
444+
if(throwException)
445+
{
446+
Assert.IsType<FormatException>(ex);
447+
Assert.Equal("Test", ex.Message);
448+
}
449+
else
450+
{
451+
var tce = Assert.IsType<TaskCanceledException>(ex);
452+
Assert.Equal(token, tce.CancellationToken);
453+
}
407454
Assert.Equal(1, readNotCancelledCount);
408455
Assert.Equal(0, readCancelledCount);
409456
}

src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/GenericHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task Invoke(HttpContext context)
5151

5252
if (path.Equals(new PathString("/statuscode.ashx")))
5353
{
54-
StatusCodeHandler.Invoke(context);
54+
await StatusCodeHandler.InvokeAsync(context);
5555
return;
5656
}
5757

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,37 @@
33

44
using System;
55
using Microsoft.AspNetCore.Http;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using System.Net.Http;
69

710
namespace NetCoreServer
811
{
912
public class StatusCodeHandler
1013
{
11-
public static void Invoke(HttpContext context)
14+
public static async Task InvokeAsync(HttpContext context)
1215
{
1316
string statusCodeString = context.Request.Query["statuscode"];
1417
string statusDescription = context.Request.Query["statusdescription"];
18+
string delayString = context.Request.Query["delay"];
1519
try
1620
{
1721
int statusCode = int.Parse(statusCodeString);
22+
int delay = string.IsNullOrWhiteSpace(delayString) ? 0 : int.Parse(delayString);
23+
1824
context.Response.StatusCode = statusCode;
19-
context.Response.SetStatusDescription(
20-
string.IsNullOrWhiteSpace(statusDescription) ? " " : statusDescription);
25+
context.Response.SetStatusDescription(string.IsNullOrWhiteSpace(statusDescription) ? " " : statusDescription);
26+
27+
if (delay > 0)
28+
{
29+
var buffer = new byte[1];
30+
if (context.Request.Method == HttpMethod.Post.Method)
31+
{
32+
await context.Request.Body.ReadExactlyAsync(buffer, CancellationToken.None);
33+
}
34+
await context.Response.StartAsync(CancellationToken.None);
35+
await Task.Delay(delay);
36+
}
2137
}
2238
catch (Exception)
2339
{

src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public BrowserHttpController(HttpRequestMessage request, bool? allowAutoRedirect
160160

161161
if (!_httpController.IsDisposed)
162162
{
163-
BrowserHttpInterop.AbortRequest(_httpController);
163+
BrowserHttpInterop.Abort(_httpController);
164164
}
165165
}, httpController);
166166

@@ -248,9 +248,16 @@ public async Task<HttpResponseMessage> CallFetch()
248248
{
249249
fetchPromise = BrowserHttpInterop.FetchStream(_jsController, uri, _headerNames, _headerValues, _optionNames, _optionValues);
250250
writeStream = new BrowserHttpWriteStream(this);
251-
await _request.Content.CopyToAsync(writeStream, _cancellationToken).ConfigureAwait(false);
252-
var closePromise = BrowserHttpInterop.TransformStreamClose(_jsController);
253-
await BrowserHttpInterop.CancellationHelper(closePromise, _cancellationToken, _jsController).ConfigureAwait(false);
251+
try
252+
{
253+
await _request.Content.CopyToAsync(writeStream, _cancellationToken).ConfigureAwait(false);
254+
var closePromise = BrowserHttpInterop.TransformStreamClose(_jsController);
255+
await BrowserHttpInterop.CancellationHelper(closePromise, _cancellationToken, _jsController).ConfigureAwait(false);
256+
}
257+
catch(JSException jse) when (jse.Message.Contains("BrowserHttpWriteStream.Rejected", StringComparison.Ordinal))
258+
{
259+
// any error from pushing bytes will also appear in the fetch promise result
260+
}
254261
}
255262
else
256263
{
@@ -344,7 +351,7 @@ public void Dispose()
344351
{
345352
if (!_jsController.IsDisposed)
346353
{
347-
BrowserHttpInterop.AbortRequest(_jsController);// aborts also response
354+
BrowserHttpInterop.Abort(_jsController);// aborts also response
348355
}
349356
_jsController.Dispose();
350357
}

src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,8 @@ internal static partial class BrowserHttpInterop
2020
[JSImport("INTERNAL.http_wasm_create_controller")]
2121
public static partial JSObject CreateController();
2222

23-
[JSImport("INTERNAL.http_wasm_abort_request")]
24-
public static partial void AbortRequest(
25-
JSObject httpController);
26-
27-
[JSImport("INTERNAL.http_wasm_abort_response")]
28-
public static partial void AbortResponse(
29-
JSObject httpController);
23+
[JSImport("INTERNAL.http_wasm_abort")]
24+
public static partial void Abort(JSObject httpController);
3025

3126
[JSImport("INTERNAL.http_wasm_transform_stream_write")]
3227
public static partial Task TransformStreamWrite(
@@ -143,7 +138,7 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc
143138
CancelablePromise.CancelPromise(_promise);
144139
if (!_jsController.IsDisposed)
145140
{
146-
AbortResponse(_jsController);
141+
Abort(_jsController);
147142
}
148143
}, (promise, jsController)))
149144
{
@@ -160,6 +155,10 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc
160155
{
161156
throw Http.CancellationHelper.CreateOperationCanceledException(jse, CancellationToken.None);
162157
}
158+
if (jse.Message.Contains("BrowserHttpWriteStream.Rejected", StringComparison.Ordinal))
159+
{
160+
throw; // do not translate
161+
}
163162
Http.CancellationHelper.ThrowIfCancellationRequested(jse, cancellationToken);
164163
throw new HttpRequestException(jse.Message, jse);
165164
}

src/mono/browser/runtime/exports-internal.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads";
66
import { MonoObjectNull, type MonoObject } from "./types/internal";
77
import cwraps, { profiler_c_functions, threads_c_functions as twraps } from "./cwraps";
88
import { mono_wasm_send_dbg_command_with_parms, mono_wasm_send_dbg_command, mono_wasm_get_dbg_command_info, mono_wasm_get_details, mono_wasm_release_object, mono_wasm_call_function_on, mono_wasm_debugger_resume, mono_wasm_detach_debugger, mono_wasm_raise_debug_event, mono_wasm_change_debugger_log_level, mono_wasm_debugger_attached } from "./debug";
9-
import { http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, http_wasm_create_controller, http_wasm_abort_request, http_wasm_abort_response, http_wasm_transform_stream_write, http_wasm_transform_stream_close, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_bytes, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes, http_wasm_get_response_type, http_wasm_get_response_status } from "./http";
9+
import { http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, http_wasm_create_controller, http_wasm_abort, http_wasm_transform_stream_write, http_wasm_transform_stream_close, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_bytes, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes, http_wasm_get_response_type, http_wasm_get_response_status } from "./http";
1010
import { exportedRuntimeAPI, Module, runtimeHelpers } from "./globals";
1111
import { get_property, set_property, has_property, get_typeof_property, get_global_this, dynamic_import } from "./invoke-js";
1212
import { mono_wasm_stringify_as_error_with_stack } from "./logging";
@@ -80,8 +80,7 @@ export function export_internal (): any {
8080
http_wasm_create_controller,
8181
http_wasm_get_response_type,
8282
http_wasm_get_response_status,
83-
http_wasm_abort_request,
84-
http_wasm_abort_response,
83+
http_wasm_abort,
8584
http_wasm_transform_stream_write,
8685
http_wasm_transform_stream_close,
8786
http_wasm_fetch,

src/mono/browser/runtime/http.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,31 @@ export function http_wasm_create_controller (): HttpController {
7272
return controller;
7373
}
7474

75-
export function http_wasm_abort_request (controller: HttpController): void {
76-
try {
77-
if (controller.streamWriter) {
78-
controller.streamWriter.abort();
75+
function handle_abort_error (promise:Promise<any>) {
76+
promise.catch((err) => {
77+
if (err && err !== "AbortError" && err.name !== "AbortError" ) {
78+
Module.err("Unexpected error: " + err);
7979
}
80-
} catch (err) {
81-
// ignore
82-
}
83-
http_wasm_abort_response(controller);
80+
// otherwise, it's expected
81+
});
8482
}
8583

86-
export function http_wasm_abort_response (controller: HttpController): void {
84+
export function http_wasm_abort (controller: HttpController): void {
8785
if (BuildConfiguration === "Debug") commonAsserts(controller);
8886
try {
89-
controller.isAborted = true;
90-
if (controller.streamReader) {
91-
controller.streamReader.cancel().catch((err) => {
92-
if (err && err.name !== "AbortError") {
93-
Module.err("Error in http_wasm_abort_response: " + err);
94-
}
95-
// otherwise, it's expected
96-
});
87+
if (!controller.isAborted) {
88+
if (controller.streamWriter) {
89+
handle_abort_error(controller.streamWriter.abort());
90+
controller.isAborted = true;
91+
}
92+
if (controller.streamReader) {
93+
handle_abort_error(controller.streamReader.cancel());
94+
controller.isAborted = true;
95+
}
96+
}
97+
if (!controller.isAborted) {
98+
controller.abortController.abort("AbortError");
9799
}
98-
controller.abortController.abort();
99100
} catch (err) {
100101
// ignore
101102
}
@@ -110,9 +111,12 @@ export function http_wasm_transform_stream_write (controller: HttpController, bu
110111
return wrap_as_cancelable_promise(async () => {
111112
mono_assert(controller.streamWriter, "expected streamWriter");
112113
mono_assert(controller.responsePromise, "expected fetch promise");
113-
// race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250
114-
await Promise.race([controller.streamWriter.ready, controller.responsePromise]);
115-
await Promise.race([controller.streamWriter.write(copy), controller.responsePromise]);
114+
try {
115+
await controller.streamWriter.ready;
116+
await controller.streamWriter.write(copy);
117+
} catch (ex) {
118+
throw new Error("BrowserHttpWriteStream.Rejected");
119+
}
116120
});
117121
}
118122

@@ -121,16 +125,21 @@ export function http_wasm_transform_stream_close (controller: HttpController): C
121125
return wrap_as_cancelable_promise(async () => {
122126
mono_assert(controller.streamWriter, "expected streamWriter");
123127
mono_assert(controller.responsePromise, "expected fetch promise");
124-
// race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250
125-
await Promise.race([controller.streamWriter.ready, controller.responsePromise]);
126-
await Promise.race([controller.streamWriter.close(), controller.responsePromise]);
128+
try {
129+
await controller.streamWriter.ready;
130+
await controller.streamWriter.close();
131+
} catch (ex) {
132+
throw new Error("BrowserHttpWriteStream.Rejected");
133+
}
127134
});
128135
}
129136

130137
export function http_wasm_fetch_stream (controller: HttpController, url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[]): ControllablePromise<void> {
131138
if (BuildConfiguration === "Debug") commonAsserts(controller);
132139
const transformStream = new TransformStream<Uint8Array, Uint8Array>();
133140
controller.streamWriter = transformStream.writable.getWriter();
141+
handle_abort_error(controller.streamWriter.closed);
142+
handle_abort_error(controller.streamWriter.ready);
134143
const fetch_promise = http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, transformStream.readable);
135144
return fetch_promise;
136145
}

src/mono/browser/runtime/loader/exit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,11 @@ function logOnExit (exit_code: number, reason: any) {
291291
}
292292
}
293293
}
294-
function unhandledrejection_handler (event: any) {
294+
function unhandledrejection_handler (event: PromiseRejectionEvent) {
295295
fatal_handler(event, event.reason, "rejection");
296296
}
297297

298-
function error_handler (event: any) {
298+
function error_handler (event: ErrorEvent) {
299299
fatal_handler(event, event.error, "error");
300300
}
301301

0 commit comments

Comments
 (0)