* 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
This commit is contained in:
parent
e801707706
commit
b5128a7efd
|
|
@ -91,6 +91,12 @@ export const monoPlatform: Platform = {
|
||||||
return mono_string(jsString);
|
return mono_string(jsString);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toUint8Array: function toUint8Array(array: System_Array<any>): 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<any>): number {
|
getArrayLength: function getArrayLength(array: System_Array<any>): number {
|
||||||
return Module.getValue(getArrayDataPointer(array), 'i32');
|
return Module.getValue(getArrayDataPointer(array), 'i32');
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
toJavaScriptString(dotNetString: System_String): string;
|
toJavaScriptString(dotNetString: System_String): string;
|
||||||
toDotNetString(javaScriptString: string): System_String;
|
toDotNetString(javaScriptString: string): System_String;
|
||||||
|
|
||||||
|
toUint8Array(array: System_Array<any>): Uint8Array;
|
||||||
|
|
||||||
getArrayLength(array: System_Array<any>): number;
|
getArrayLength(array: System_Array<any>): number;
|
||||||
getArrayEntryPtr<TPtr extends Pointer>(array: System_Array<TPtr>, index: number, itemSize: number): TPtr;
|
getArrayEntryPtr<TPtr extends Pointer>(array: System_Array<TPtr>, index: number, itemSize: number): TPtr;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,71 @@
|
||||||
import { registerFunction } from '../Interop/RegisteredFunction';
|
import { registerFunction } from '../Interop/RegisteredFunction';
|
||||||
import { platform } from '../Environment';
|
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 httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser';
|
||||||
const httpClientNamespace = `${httpClientAssembly}.Http`;
|
const httpClientNamespace = `${httpClientAssembly}.Http`;
|
||||||
const httpClientTypeName = 'BrowserHttpMessageHandler';
|
const httpClientTypeName = 'BrowserHttpMessageHandler';
|
||||||
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
|
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
|
||||||
let receiveResponseMethod: MethodHandle;
|
let receiveResponseMethod: MethodHandle;
|
||||||
|
let allocateArrayMethod: MethodHandle;
|
||||||
|
|
||||||
registerFunction(`${httpClientFullTypeName}.Send`, (id: number, method: string, requestUri: string, body: string | null, headersJson: string | null, fetchArgs: RequestInit | null) => {
|
registerFunction(`${httpClientFullTypeName}.Send`, (id: number, body: System_Array<any>, jsonFetchArgs: System_String) => {
|
||||||
sendAsync(id, method, requestUri, body, headersJson, fetchArgs);
|
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<any>, jsonFetchArgs: System_String) {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
let responseText: string;
|
let responseData: ArrayBuffer;
|
||||||
|
|
||||||
const requestInit: RequestInit = fetchArgs || {};
|
const fetchOptions: FetchOptions = JSON.parse(platform.toJavaScriptString(jsonFetchArgs));
|
||||||
requestInit.method = method;
|
const requestInit: RequestInit = Object.assign(fetchOptions.requestInit, fetchOptions.requestInitOverrides);
|
||||||
requestInit.body = body || undefined;
|
|
||||||
|
if (body) {
|
||||||
|
requestInit.body = platform.toUint8Array(body);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
requestInit.headers = headersJson ? (JSON.parse(headersJson) as string[][]) : undefined;
|
response = await fetch(fetchOptions.requestUri, requestInit);
|
||||||
|
responseData = await response.arrayBuffer();
|
||||||
response = await fetch(requestUri, requestInit);
|
|
||||||
responseText = await response.text();
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
dispatchErrorResponse(id, ex.toString());
|
dispatchErrorResponse(id, ex.toString());
|
||||||
return;
|
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 = {
|
const responseDescriptor: ResponseDescriptor = {
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
headers: []
|
headers: []
|
||||||
};
|
};
|
||||||
response.headers.forEach((value, name) => {
|
response.headers.forEach((value, name) => {
|
||||||
responseDescriptor.headers.push([name, value]);
|
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<any>;
|
||||||
|
|
||||||
|
// get an Uint8Array view of it
|
||||||
|
const array = platform.toUint8Array(dotNetArray);
|
||||||
|
|
||||||
|
// copy the responseData to our managed byte[]
|
||||||
|
array.set(new Uint8Array(responseData));
|
||||||
|
|
||||||
dispatchResponse(
|
dispatchResponse(
|
||||||
id,
|
id,
|
||||||
platform.toDotNetString(JSON.stringify(responseDescriptor)),
|
platform.toDotNetString(JSON.stringify(responseDescriptor)),
|
||||||
platform.toDotNetString(responseText), // TODO: Consider how to handle non-string responses
|
dotNetArray,
|
||||||
/* errorMessage */ null
|
/* 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<any> | null, errorMessage: System_String | null) {
|
||||||
if (!receiveResponseMethod) {
|
if (!receiveResponseMethod) {
|
||||||
receiveResponseMethod = platform.findMethod(
|
receiveResponseMethod = platform.findMethod(
|
||||||
httpClientAssembly,
|
httpClientAssembly,
|
||||||
|
|
@ -71,16 +92,23 @@ function dispatchResponse(id: number, responseDescriptor: System_String | null,
|
||||||
platform.callMethod(receiveResponseMethod, null, [
|
platform.callMethod(receiveResponseMethod, null, [
|
||||||
platform.toDotNetString(id.toString()),
|
platform.toDotNetString(id.toString()),
|
||||||
responseDescriptor,
|
responseDescriptor,
|
||||||
responseText,
|
responseData,
|
||||||
errorMessage,
|
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 {
|
interface ResponseDescriptor {
|
||||||
// We don't have BodyText in here because if we did, then in the JSON-response case (which
|
// 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
|
// 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.
|
// also gets JSON encoded. It would work but is twice the amount of string processing.
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
|
statusText: string;
|
||||||
headers: string[][];
|
headers: string[][];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,38 +49,39 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Http
|
||||||
_pendingRequests.Add(id, tcs);
|
_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<object>(
|
options.RequestInit = new RequestInit
|
||||||
|
{
|
||||||
|
Credentials = GetDefaultCredentialsString(),
|
||||||
|
Headers = GetHeadersAsStringArray(request),
|
||||||
|
Method = request.Method.Method
|
||||||
|
};
|
||||||
|
|
||||||
|
options.RequestUri = request.RequestUri.ToString();
|
||||||
|
|
||||||
|
RegisteredFunction.InvokeUnmarshalled<int, byte[], string, object>(
|
||||||
$"{typeof(BrowserHttpMessageHandler).FullName}.Send",
|
$"{typeof(BrowserHttpMessageHandler).FullName}.Send",
|
||||||
id,
|
id,
|
||||||
request.Method.Method,
|
request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(),
|
||||||
request.RequestUri,
|
JsonUtil.Serialize(options));
|
||||||
request.Content == null ? null : await GetContentAsString(request.Content),
|
|
||||||
SerializeHeadersAsJson(request),
|
|
||||||
fetchArgs ?? CreateDefaultFetchArgs());
|
|
||||||
|
|
||||||
return await tcs.Task;
|
return await tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SerializeHeadersAsJson(HttpRequestMessage request)
|
private string[][] GetHeadersAsStringArray(HttpRequestMessage request)
|
||||||
=> JsonUtil.Serialize(
|
=> (from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
|
||||||
(from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
|
from headerValue in header.Value // There can be more than one value for each name
|
||||||
from headerValue in header.Value // There can be more than one value for each name
|
select new[] { header.Key, headerValue }).ToArray();
|
||||||
select new[] { header.Key, headerValue }).ToList()
|
|
||||||
);
|
|
||||||
|
|
||||||
private static async Task<string> GetContentAsString(HttpContent content)
|
|
||||||
=> content is StringContent stringContent
|
|
||||||
? await stringContent.ReadAsStringAsync()
|
|
||||||
: throw new InvalidOperationException($"Currently, {typeof(HttpClient).FullName} " +
|
|
||||||
$"only supports contents of type {nameof(StringContent)}, but you supplied " +
|
|
||||||
$"{content.GetType().FullName}.");
|
|
||||||
|
|
||||||
private static void ReceiveResponse(
|
private static void ReceiveResponse(
|
||||||
string id,
|
string id,
|
||||||
string responseDescriptorJson,
|
string responseDescriptorJson,
|
||||||
string responseBodyText,
|
byte[] responseBodyData,
|
||||||
string errorText)
|
string errorText)
|
||||||
{
|
{
|
||||||
TaskCompletionSource<HttpResponseMessage> tcs;
|
TaskCompletionSource<HttpResponseMessage> tcs;
|
||||||
|
|
@ -98,16 +99,18 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Http
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var responseDescriptor = JsonUtil.Deserialize<ResponseDescriptor>(responseDescriptorJson);
|
var responseDescriptor = JsonUtil.Deserialize<ResponseDescriptor>(responseDescriptorJson);
|
||||||
var responseContent = responseBodyText == null ? null : new StringContent(responseBodyText);
|
var responseContent = responseBodyData == null ? null : new ByteArrayContent(responseBodyData);
|
||||||
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
|
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
|
||||||
tcs.SetResult(responseMessage);
|
tcs.SetResult(responseMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object CreateDefaultFetchArgs()
|
private static byte[] AllocateArray(string length)
|
||||||
=> new { credentials = GetDefaultCredentialsString() };
|
{
|
||||||
|
return new byte[int.Parse(length)];
|
||||||
|
}
|
||||||
|
|
||||||
private static object GetDefaultCredentialsString()
|
private static string GetDefaultCredentialsString()
|
||||||
{
|
{
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
|
||||||
// standard values and meanings
|
// standard values and meanings
|
||||||
|
|
@ -124,17 +127,33 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep in sync with TypeScript class in Http.ts
|
// Keep these in sync with TypeScript class in Http.ts
|
||||||
|
private class FetchOptions
|
||||||
|
{
|
||||||
|
public string RequestUri { get; set; }
|
||||||
|
public RequestInit RequestInit { get; set; }
|
||||||
|
public object RequestInitOverrides { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RequestInit
|
||||||
|
{
|
||||||
|
public string Credentials { get; set; }
|
||||||
|
public string[][] Headers { get; set; }
|
||||||
|
public string Method { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private class ResponseDescriptor
|
private class ResponseDescriptor
|
||||||
{
|
{
|
||||||
#pragma warning disable 0649
|
#pragma warning disable 0649
|
||||||
public int StatusCode { get; set; }
|
public int StatusCode { get; set; }
|
||||||
|
public string StatusText { get; set; }
|
||||||
public string[][] Headers { get; set; }
|
public string[][] Headers { get; set; }
|
||||||
#pragma warning restore 0649
|
#pragma warning restore 0649
|
||||||
|
|
||||||
public HttpResponseMessage ToResponseMessage(HttpContent content)
|
public HttpResponseMessage ToResponseMessage(HttpContent content)
|
||||||
{
|
{
|
||||||
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
|
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
|
||||||
|
result.ReasonPhrase = StatusText;
|
||||||
result.Content = content;
|
result.Content = content;
|
||||||
var headers = result.Headers;
|
var headers = result.Headers;
|
||||||
var contentHeaders = result.Content?.Headers;
|
var contentHeaders = result.Content?.Headers;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using BasicTestApp.HttpClientTest;
|
||||||
|
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Support.UI;
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
|
{
|
||||||
|
public class BinaryHttpClientTest : BasicTestAppTestBase, IClassFixture<AspNetSiteServerFixture>
|
||||||
|
{
|
||||||
|
readonly ServerFixture _apiServerFixture;
|
||||||
|
readonly IWebElement _appElement;
|
||||||
|
IWebElement _responseStatus;
|
||||||
|
IWebElement _responseStatusText;
|
||||||
|
IWebElement _testOutcome;
|
||||||
|
|
||||||
|
public BinaryHttpClientTest(
|
||||||
|
BrowserFixture browserFixture,
|
||||||
|
DevHostServerFixture<BasicTestApp.Program> devHostServerFixture,
|
||||||
|
AspNetSiteServerFixture apiServerFixture,
|
||||||
|
ITestOutputHelper output)
|
||||||
|
: base(browserFixture, devHostServerFixture, output)
|
||||||
|
{
|
||||||
|
apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||||
|
_apiServerFixture = apiServerFixture;
|
||||||
|
|
||||||
|
Navigate(ServerPathBase, noReload: true);
|
||||||
|
_appElement = MountTestComponent<BinaryHttpRequestsComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSendAndReceiveBytes()
|
||||||
|
{
|
||||||
|
IssueRequest("/api/data");
|
||||||
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
|
Assert.Equal("OK", _responseStatusText.Text);
|
||||||
|
Assert.Equal("", _testOutcome.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IssueRequest(string relativeUri)
|
||||||
|
{
|
||||||
|
var targetUri = new Uri(_apiServerFixture.RootUri, relativeUri);
|
||||||
|
SetValue("request-uri", targetUri.AbsoluteUri);
|
||||||
|
|
||||||
|
_appElement.FindElement(By.Id("send-request")).Click();
|
||||||
|
|
||||||
|
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
|
||||||
|
driver => driver.FindElement(By.Id("response-status")) != null);
|
||||||
|
_responseStatus = _appElement.FindElement(By.Id("response-status"));
|
||||||
|
_responseStatusText = _appElement.FindElement(By.Id("response-status-text"));
|
||||||
|
_testOutcome = _appElement.FindElement(By.Id("test-outcome"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetValue(string elementId, string value)
|
||||||
|
{
|
||||||
|
var element = Browser.FindElement(By.Id(elementId));
|
||||||
|
element.Clear();
|
||||||
|
element.SendKeys(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
AddRequestHeader("Content-Type", "application/json");
|
AddRequestHeader("Content-Type", "application/json");
|
||||||
IssueRequest("PUT", "/api/person", "{\"Name\": \"Bert\", \"Id\": 123}");
|
IssueRequest("PUT", "/api/person", "{\"Name\": \"Bert\", \"Id\": 123}");
|
||||||
Assert.Equal("OK", _responseStatus.Text);
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
Assert.Contains("Content-Type: application/json", _responseHeaders.Text);
|
Assert.Contains("Content-Type: application/json", _responseHeaders.Text, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Equal("{\"id\":123,\"name\":\"Bert\"}", _responseBody.Text);
|
Assert.Equal("{\"id\":123,\"name\":\"Bert\"}", _responseBody.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
@using System.Net
|
||||||
|
@using System.Net.Http
|
||||||
|
@inject HttpClient Http
|
||||||
|
|
||||||
|
<h1>Binary HTTP request tester</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div>URI:</div>
|
||||||
|
<input id="request-uri" bind="@uri" size="60"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button id="send-request" onclick="@DoRequest">Request</button>
|
||||||
|
|
||||||
|
@if (responseStatusCode.HasValue)
|
||||||
|
{
|
||||||
|
<h2>Response</h2>
|
||||||
|
<p><div>Status:</div><span id="response-status">@responseStatusCode</span></p>
|
||||||
|
<p><div>StatusText:</div><span id="response-status-text">@responseStatusText</span></p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span id="test-outcome">@testOutcome</span>
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -88,6 +88,15 @@
|
||||||
|
|
||||||
foreach (var header in requestHeaders)
|
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))
|
if (!requestMessage.Headers.TryAddWithoutValidation(header.Name, header.Value))
|
||||||
{
|
{
|
||||||
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value);
|
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
||||||
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
||||||
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||||
|
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>
|
||||||
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
||||||
<option value="BasicTestApp.BindCasesComponent">@bind cases</option>
|
<option value="BasicTestApp.BindCasesComponent">@bind cases</option>
|
||||||
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
|
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
|
||||||
|
|
|
||||||
|
|
@ -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<IActionResult> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue