From b5128a7efd20b09bf8268722c2bada65b507b7da Mon Sep 17 00:00:00 2001
From: Robin Sue
Date: Wed, 23 May 2018 11:16:27 +0200
Subject: [PATCH] Add support for sending and receiving arbitrary HttpContent,
refs #479 (#815)
* Add support for zero copy byte array marshalling
* Add support for sending arbitrary HttpContent, refs #479
* Fix unit test to set ContentType correctly
* Add support for receiving binary data
* Compare header case insensitive
* Add unit test for binary http requests
---
.../src/Platform/Mono/MonoPlatform.ts | 6 ++
.../src/Platform/Platform.ts | 2 +
.../src/Services/Http.ts | 64 ++++++++++++-----
.../Http/BrowserHttpMessageHandler.cs | 71 ++++++++++++-------
.../Tests/BinaryHttpClientTest.cs | 67 +++++++++++++++++
.../Tests/HttpClientTest.cs | 2 +-
.../BinaryHttpRequestsComponent.cshtml | 71 +++++++++++++++++++
.../HttpRequestsComponent.cshtml | 9 +++
test/testapps/BasicTestApp/wwwroot/index.html | 1 +
.../TestServer/Controllers/DataController.cs | 46 ++++++++++++
10 files changed, 294 insertions(+), 45 deletions(-)
create mode 100644 test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/BinaryHttpClientTest.cs
create mode 100644 test/testapps/BasicTestApp/HttpClientTest/BinaryHttpRequestsComponent.cshtml
create mode 100644 test/testapps/TestServer/Controllers/DataController.cs
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
index c4d59e4b5a..986007811b 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
+++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
@@ -91,6 +91,12 @@ export const monoPlatform: Platform = {
return mono_string(jsString);
},
+ toUint8Array: function toUint8Array(array: System_Array): Uint8Array {
+ const dataPtr = getArrayDataPointer(array);
+ const length = Module.getValue(dataPtr, 'i32');
+ return new Uint8Array(Module.HEAPU8.buffer, dataPtr + 4, length);
+ },
+
getArrayLength: function getArrayLength(array: System_Array): number {
return Module.getValue(getArrayDataPointer(array), 'i32');
},
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts
index fd1b29d727..690b71f758 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts
+++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts
@@ -8,6 +8,8 @@
toJavaScriptString(dotNetString: System_String): string;
toDotNetString(javaScriptString: string): System_String;
+ toUint8Array(array: System_Array): Uint8Array;
+
getArrayLength(array: System_Array): number;
getArrayEntryPtr(array: System_Array, index: number, itemSize: number): TPtr;
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts
index f9e950c61f..350d96c60c 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts
+++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts
@@ -1,50 +1,71 @@
import { registerFunction } from '../Interop/RegisteredFunction';
import { platform } from '../Environment';
-import { MethodHandle, System_String } from '../Platform/Platform';
+import { MethodHandle, System_String, System_Array } from '../Platform/Platform';
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser';
const httpClientNamespace = `${httpClientAssembly}.Http`;
const httpClientTypeName = 'BrowserHttpMessageHandler';
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
let receiveResponseMethod: MethodHandle;
+let allocateArrayMethod: MethodHandle;
-registerFunction(`${httpClientFullTypeName}.Send`, (id: number, method: string, requestUri: string, body: string | null, headersJson: string | null, fetchArgs: RequestInit | null) => {
- sendAsync(id, method, requestUri, body, headersJson, fetchArgs);
+registerFunction(`${httpClientFullTypeName}.Send`, (id: number, body: System_Array, jsonFetchArgs: System_String) => {
+ sendAsync(id, body, jsonFetchArgs);
});
-async function sendAsync(id: number, method: string, requestUri: string, body: string | null, headersJson: string | null, fetchArgs: RequestInit | null) {
+async function sendAsync(id: number, body: System_Array, jsonFetchArgs: System_String) {
let response: Response;
- let responseText: string;
+ let responseData: ArrayBuffer;
- const requestInit: RequestInit = fetchArgs || {};
- requestInit.method = method;
- requestInit.body = body || undefined;
+ const fetchOptions: FetchOptions = JSON.parse(platform.toJavaScriptString(jsonFetchArgs));
+ const requestInit: RequestInit = Object.assign(fetchOptions.requestInit, fetchOptions.requestInitOverrides);
+
+ if (body) {
+ requestInit.body = platform.toUint8Array(body);
+ }
try {
- requestInit.headers = headersJson ? (JSON.parse(headersJson) as string[][]) : undefined;
-
- response = await fetch(requestUri, requestInit);
- responseText = await response.text();
+ response = await fetch(fetchOptions.requestUri, requestInit);
+ responseData = await response.arrayBuffer();
} catch (ex) {
dispatchErrorResponse(id, ex.toString());
return;
}
- dispatchSuccessResponse(id, response, responseText);
+ dispatchSuccessResponse(id, response, responseData);
}
-function dispatchSuccessResponse(id: number, response: Response, responseText: string) {
+function dispatchSuccessResponse(id: number, response: Response, responseData: ArrayBuffer) {
const responseDescriptor: ResponseDescriptor = {
statusCode: response.status,
+ statusText: response.statusText,
headers: []
};
response.headers.forEach((value, name) => {
responseDescriptor.headers.push([name, value]);
});
+ if (!allocateArrayMethod) {
+ allocateArrayMethod = platform.findMethod(
+ httpClientAssembly,
+ httpClientNamespace,
+ httpClientTypeName,
+ 'AllocateArray'
+ );
+ }
+
+ // allocate a managed byte[] of the right size
+ const dotNetArray = platform.callMethod(allocateArrayMethod, null, [platform.toDotNetString(responseData.byteLength.toString())]) as System_Array;
+
+ // get an Uint8Array view of it
+ const array = platform.toUint8Array(dotNetArray);
+
+ // copy the responseData to our managed byte[]
+ array.set(new Uint8Array(responseData));
+
dispatchResponse(
id,
platform.toDotNetString(JSON.stringify(responseDescriptor)),
- platform.toDotNetString(responseText), // TODO: Consider how to handle non-string responses
+ dotNetArray,
/* errorMessage */ null
);
}
@@ -58,7 +79,7 @@ function dispatchErrorResponse(id: number, errorMessage: string) {
);
}
-function dispatchResponse(id: number, responseDescriptor: System_String | null, responseText: System_String | null, errorMessage: System_String | null) {
+function dispatchResponse(id: number, responseDescriptor: System_String | null, responseData: System_Array | null, errorMessage: System_String | null) {
if (!receiveResponseMethod) {
receiveResponseMethod = platform.findMethod(
httpClientAssembly,
@@ -71,16 +92,23 @@ function dispatchResponse(id: number, responseDescriptor: System_String | null,
platform.callMethod(receiveResponseMethod, null, [
platform.toDotNetString(id.toString()),
responseDescriptor,
- responseText,
+ responseData,
errorMessage,
]);
}
-// Keep this in sync with the .NET equivalent in HttpClient.cs
+// Keep these in sync with the .NET equivalent in BrowserHttpMessageHandler.cs
+interface FetchOptions {
+ requestUri: string;
+ requestInit: RequestInit;
+ requestInitOverrides: RequestInit;
+}
+
interface ResponseDescriptor {
// We don't have BodyText in here because if we did, then in the JSON-response case (which
// is the most common case), we'd be double-encoding it, since the entire ResponseDescriptor
// also gets JSON encoded. It would work but is twice the amount of string processing.
statusCode: number;
+ statusText: string;
headers: string[][];
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs
index 9511a1a2c2..980b90fba1 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs
@@ -49,38 +49,39 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Http
_pendingRequests.Add(id, tcs);
}
- request.Properties.TryGetValue(FetchArgs, out var fetchArgs);
+ var options = new FetchOptions();
+ if (request.Properties.TryGetValue(FetchArgs, out var fetchArgs))
+ {
+ options.RequestInitOverrides = fetchArgs;
+ }
- RegisteredFunction.Invoke
+
+
+
+@if (responseStatusCode.HasValue)
+{
+ Response
+ Status:
@responseStatusCode
+ StatusText:
@responseStatusText
+}
+
+@testOutcome
+
+@functions {
+ string uri = "";
+ HttpStatusCode? responseStatusCode;
+ string responseStatusText;
+ string testOutcome = "";
+
+ async Task DoRequest()
+ {
+ responseStatusCode = null;
+ responseStatusText = null;
+ testOutcome = null;
+
+ try
+ {
+ var bytes = await Http.GetByteArrayAsync(uri);
+ if (bytes.Length != 256)
+ {
+ testOutcome = "Expected 256 bytes but got " + bytes.Length.ToString();
+ return;
+ }
+
+ var reversedBytes = bytes.ToArray();
+ Array.Reverse(reversedBytes);
+
+ var response = await Http.PostAsync(uri, new ByteArrayContent(reversedBytes));
+ responseStatusCode = response.StatusCode;
+ responseStatusText = response.ReasonPhrase;
+ var doubleReversed = await response.Content.ReadAsByteArrayAsync();
+
+ for (int i = 0; i <= byte.MaxValue; i++)
+ {
+ if (doubleReversed[i] != (byte)i)
+ {
+ testOutcome = $"Expected byte at index {i} to have value {i} but actually was {doubleReversed[i]}";
+ return;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ if (ex is AggregateException)
+ {
+ ex = ex.InnerException;
+ }
+ responseStatusCode = HttpStatusCode.SeeOther;
+ testOutcome = ex.Message + Environment.NewLine + ex.StackTrace;
+ }
+ }
+}
diff --git a/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml b/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml
index f49ae4d75a..0bf537df1c 100644
--- a/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml
+++ b/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml
@@ -88,6 +88,15 @@
foreach (var header in requestHeaders)
{
+ // StringContent automatically adds its own Content-Type header with default value "text/plain"
+ // If the developer is trying to specify a content type explicitly, we need to replace the default value,
+ // rather than adding a second Content-Type header.
+ if (header.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && requestMessage.Content != null)
+ {
+ requestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(header.Value);
+ continue;
+ }
+
if (!requestMessage.Headers.TryAddWithoutValidation(header.Name, header.Value))
{
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value);
diff --git a/test/testapps/BasicTestApp/wwwroot/index.html b/test/testapps/BasicTestApp/wwwroot/index.html
index d4f052e4ea..9ac33fb8e0 100644
--- a/test/testapps/BasicTestApp/wwwroot/index.html
+++ b/test/testapps/BasicTestApp/wwwroot/index.html
@@ -26,6 +26,7 @@
+
diff --git a/test/testapps/TestServer/Controllers/DataController.cs b/test/testapps/TestServer/Controllers/DataController.cs
new file mode 100644
index 0000000000..1ae4fd6198
--- /dev/null
+++ b/test/testapps/TestServer/Controllers/DataController.cs
@@ -0,0 +1,46 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Mvc;
+
+namespace TestServer.Controllers
+{
+ [EnableCors("AllowAll")]
+ [Route("api/[controller]")]
+ public class DataController : Controller
+ {
+ // GET api/data
+ [HttpGet]
+ public FileContentResult Get()
+ {
+ var bytes = new byte[byte.MaxValue + 1];
+ for (int i = 0; i <= byte.MaxValue; i++)
+ {
+ bytes[i] = (byte)i;
+ }
+
+ return File(bytes, "application/octet-stream");
+ }
+
+ // POST api/data
+ [HttpPost]
+ public async Task PostAsync()
+ {
+ var ms = new MemoryStream();
+ await Request.Body.CopyToAsync(ms);
+ var bytes = ms.ToArray();
+ Array.Reverse(bytes);
+
+ for (int i = 0; i <= byte.MaxValue; i++)
+ {
+ if (bytes[i] != (byte)i)
+ {
+ return BadRequest();
+ }
+ }
+
+ return File(bytes, "application/octet-stream");
+ }
+ }
+}