Return response trailers from test server (#10135)

This commit is contained in:
James Newton-King 2019-05-11 07:50:53 +12:00 committed by GitHub
parent 1cb42b1374
commit 11311d4f9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 15 deletions

View File

@ -119,9 +119,22 @@ namespace Microsoft.AspNetCore.TestHost
responseBody = context.Response.Body; responseBody = context.Response.Body;
}); });
var response = new HttpResponseMessage();
// Copy trailers to the response message when the response stream is complete
contextBuilder.RegisterResponseReadCompleteCallback(context =>
{
var responseTrailersFeature = context.Features.Get<IHttpResponseTrailersFeature>();
foreach (var trailer in responseTrailersFeature.Trailers)
{
bool success = response.TrailingHeaders.TryAddWithoutValidation(trailer.Key, (IEnumerable<string>)trailer.Value);
Contract.Assert(success, "Bad trailer");
}
});
var httpContext = await contextBuilder.SendAsync(cancellationToken); var httpContext = await contextBuilder.SendAsync(cancellationToken);
var response = new HttpResponseMessage();
response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode; response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase; response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase;
response.RequestMessage = request; response.RequestMessage = request;

View File

@ -17,13 +17,15 @@ namespace Microsoft.AspNetCore.TestHost
private readonly IHttpApplication<Context> _application; private readonly IHttpApplication<Context> _application;
private readonly bool _preserveExecutionContext; private readonly bool _preserveExecutionContext;
private readonly HttpContext _httpContext; private readonly HttpContext _httpContext;
private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously);
private ResponseStream _responseStream; private readonly ResponseStream _responseStream;
private ResponseFeature _responseFeature = new ResponseFeature(); private readonly ResponseFeature _responseFeature = new ResponseFeature();
private RequestLifetimeFeature _requestLifetimeFeature = new RequestLifetimeFeature(); private readonly RequestLifetimeFeature _requestLifetimeFeature = new RequestLifetimeFeature();
private readonly ResponseTrailersFeature _responseTrailersFeature = new ResponseTrailersFeature();
private bool _pipelineFinished; private bool _pipelineFinished;
private Context _testContext; private Context _testContext;
private Action<HttpContext> _responseReadCompleteCallback;
internal HttpContextBuilder(IHttpApplication<Context> application, bool allowSynchronousIO, bool preserveExecutionContext) internal HttpContextBuilder(IHttpApplication<Context> application, bool allowSynchronousIO, bool preserveExecutionContext)
{ {
@ -39,8 +41,9 @@ namespace Microsoft.AspNetCore.TestHost
_httpContext.Features.Set<IHttpBodyControlFeature>(this); _httpContext.Features.Set<IHttpBodyControlFeature>(this);
_httpContext.Features.Set<IHttpResponseFeature>(_responseFeature); _httpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(_requestLifetimeFeature); _httpContext.Features.Set<IHttpRequestLifetimeFeature>(_requestLifetimeFeature);
_httpContext.Features.Set<IHttpResponseTrailersFeature>(_responseTrailersFeature);
_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest, () => AllowSynchronousIO);
_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest, () => AllowSynchronousIO, () => _responseReadCompleteCallback?.Invoke(_httpContext));
_responseFeature.Body = _responseStream; _responseFeature.Body = _responseStream;
} }
@ -56,6 +59,11 @@ namespace Microsoft.AspNetCore.TestHost
configureContext(_httpContext); configureContext(_httpContext);
} }
internal void RegisterResponseReadCompleteCallback(Action<HttpContext> responseReadCompleteCallback)
{
_responseReadCompleteCallback = responseReadCompleteCallback;
}
/// <summary> /// <summary>
/// Start processing the request. /// Start processing the request.
/// </summary> /// </summary>

View File

@ -17,22 +17,24 @@ namespace Microsoft.AspNetCore.TestHost
internal class ResponseStream : Stream internal class ResponseStream : Stream
{ {
private bool _complete; private bool _complete;
private bool _readerComplete;
private bool _aborted; private bool _aborted;
private Exception _abortException; private Exception _abortException;
private SemaphoreSlim _writeLock;
private Func<Task> _onFirstWriteAsync;
private bool _firstWrite; private bool _firstWrite;
private Action _abortRequest;
private Func<bool> _allowSynchronousIO;
private Pipe _pipe = new Pipe(); private readonly SemaphoreSlim _writeLock;
private readonly Func<Task> _onFirstWriteAsync;
private readonly Action _abortRequest;
private readonly Func<bool> _allowSynchronousIO;
private readonly Action _readComplete;
private readonly Pipe _pipe = new Pipe();
internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest, Func<bool> allowSynchronousIO) internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest, Func<bool> allowSynchronousIO, Action readComplete)
{ {
_onFirstWriteAsync = onFirstWriteAsync ?? throw new ArgumentNullException(nameof(onFirstWriteAsync)); _onFirstWriteAsync = onFirstWriteAsync ?? throw new ArgumentNullException(nameof(onFirstWriteAsync));
_abortRequest = abortRequest ?? throw new ArgumentNullException(nameof(abortRequest)); _abortRequest = abortRequest ?? throw new ArgumentNullException(nameof(abortRequest));
_allowSynchronousIO = allowSynchronousIO ?? throw new ArgumentNullException(nameof(allowSynchronousIO)); _allowSynchronousIO = allowSynchronousIO ?? throw new ArgumentNullException(nameof(allowSynchronousIO));
_readComplete = readComplete;
_firstWrite = true; _firstWrite = true;
_writeLock = new SemaphoreSlim(1, 1); _writeLock = new SemaphoreSlim(1, 1);
} }
@ -108,6 +110,12 @@ namespace Microsoft.AspNetCore.TestHost
{ {
VerifyBuffer(buffer, offset, count, allowEmpty: false); VerifyBuffer(buffer, offset, count, allowEmpty: false);
CheckAborted(); CheckAborted();
if (_readerComplete)
{
return 0;
}
var registration = cancellationToken.Register(Cancel); var registration = cancellationToken.Register(Cancel);
try try
{ {
@ -116,6 +124,8 @@ namespace Microsoft.AspNetCore.TestHost
if (result.Buffer.IsEmpty && result.IsCompleted) if (result.Buffer.IsEmpty && result.IsCompleted)
{ {
_pipe.Reader.Complete(); _pipe.Reader.Complete();
_readComplete();
_readerComplete = true;
return 0; return 0;
} }

View File

@ -0,0 +1,13 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.TestHost
{
internal class ResponseTrailersFeature : IHttpResponseTrailersFeature
{
public IHeaderDictionary Trailers { get; set; } = new HeaderDictionary();
}
}

View File

@ -88,6 +88,71 @@ namespace Microsoft.AspNetCore.TestHost
return httpClient.GetAsync("https://example.com/"); return httpClient.GetAsync("https://example.com/");
} }
[Fact]
public async Task ServerTrailersSetOnResponseAfterContentRead()
{
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context =>
{
context.Response.AppendTrailer("StartTrailer", "Value!");
await context.Response.WriteAsync("Hello World");
await context.Response.Body.FlushAsync();
// Pause writing response to ensure trailers are written at the end
await tcs.Task;
await context.Response.WriteAsync("Bye World");
await context.Response.Body.FlushAsync();
context.Response.AppendTrailer("EndTrailer", "Value!");
}));
var invoker = new HttpMessageInvoker(handler);
var message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/");
var response = await invoker.SendAsync(message, CancellationToken.None);
Assert.Empty(response.TrailingHeaders);
var responseBody = await response.Content.ReadAsStreamAsync();
int read = await responseBody.ReadAsync(new byte[100], 0, 100);
Assert.Equal(11, read);
Assert.Empty(response.TrailingHeaders);
var readTask = responseBody.ReadAsync(new byte[100], 0, 100);
Assert.False(readTask.IsCompleted);
tcs.TrySetResult(null);
read = await readTask;
Assert.Equal(9, read);
Assert.Empty(response.TrailingHeaders);
// Read nothing because we're at the end of the response
read = await responseBody.ReadAsync(new byte[100], 0, 100);
Assert.Equal(0, read);
// Ensure additional reads after end don't effect trailers
read = await responseBody.ReadAsync(new byte[100], 0, 100);
Assert.Equal(0, read);
Assert.Collection(response.TrailingHeaders,
kvp =>
{
Assert.Equal("StartTrailer", kvp.Key);
Assert.Equal("Value!", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("EndTrailer", kvp.Key);
Assert.Equal("Value!", kvp.Value.Single());
});
}
[Fact] [Fact]
public async Task ResubmitRequestWorks() public async Task ResubmitRequestWorks()
{ {