diff --git a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs index 899be43f0c..cae69dd66c 100644 --- a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs +++ b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs @@ -373,6 +373,13 @@ namespace Microsoft.AspNetCore.Http public string ToUriComponent() { throw null; } } public delegate System.Threading.Tasks.Task RequestDelegate(Microsoft.AspNetCore.Http.HttpContext context); + public static partial class RequestTrailerExtensions + { + public static bool CheckTrailersAvailable(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; } + public static Microsoft.Extensions.Primitives.StringValues GetDeclaredTrailers(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; } + public static Microsoft.Extensions.Primitives.StringValues GetTrailer(this Microsoft.AspNetCore.Http.HttpRequest request, string trailerName) { throw null; } + public static bool SupportsTrailers(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; } + } public static partial class ResponseTrailerExtensions { public static void AppendTrailer(this Microsoft.AspNetCore.Http.HttpResponse response, string trailerName, Microsoft.Extensions.Primitives.StringValues trailerValues) { } diff --git a/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs new file mode 100644 index 0000000000..6ffeb2eebc --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs @@ -0,0 +1,65 @@ +// 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 System; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// HttpRequest extensions for working with request trailing headers. + /// + public static class RequestTrailerExtensions + { + /// + /// Gets the request "Trailer" header that lists which trailers to expect after the body. + /// + /// + /// + public static StringValues GetDeclaredTrailers(this HttpRequest request) + { + return request.Headers.GetCommaSeparatedValues(HeaderNames.Trailer); + } + + /// + /// Indicates if the request supports receiving trailer headers. + /// + /// + /// + public static bool SupportsTrailers(this HttpRequest request) + { + return request.HttpContext.Features.Get() != null; + } + + /// + /// Checks if the request supports trailers and they are available to be read now. + /// This does not mean that there are any trailers to read. + /// + /// + /// + public static bool CheckTrailersAvailable(this HttpRequest request) + { + return request.HttpContext.Features.Get()?.Available == true; + } + + /// + /// Gets the requested trailing header from the response. Check + /// or a NotSupportedException may be thrown. + /// Check or an InvalidOperationException may be thrown. + /// + /// + /// + public static StringValues GetTrailer(this HttpRequest request, string trailerName) + { + var feature = request.HttpContext.Features.Get(); + if (feature == null) + { + throw new NotSupportedException("This request does not support trailers."); + } + + return feature.Trailers[trailerName]; + } + } +} diff --git a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs index 48f381bb96..8c0f10b5dc 100644 --- a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs +++ b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs @@ -195,6 +195,11 @@ namespace Microsoft.AspNetCore.Http.Features System.Threading.CancellationToken RequestAborted { get; set; } void Abort(); } + public partial interface IHttpRequestTrailersFeature + { + bool Available { get; } + Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; } + } public partial interface IHttpResponseFeature { System.IO.Stream Body { get; set; } diff --git a/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs b/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs new file mode 100644 index 0000000000..19706e9e4e --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs @@ -0,0 +1,26 @@ +// 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 System; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// This feature exposes HTTP request trailer headers, either for HTTP/1.1 chunked bodies or HTTP/2 trailing headers. + /// + public interface IHttpRequestTrailersFeature + { + /// + /// Indicates if the are available yet. They may not be available until the + /// request body is fully read. + /// + bool Available { get; } + + /// + /// The trailing headers received. This will throw if + /// returns false. They may not be available until the request body is fully read. If there are no trailers this will + /// return an empty collection. + /// + IHeaderDictionary Trailers { get; } + } +} diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs index 8aa67fc8e9..1c23c2dc98 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs @@ -276,6 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public static string GetAsciiOrUTF8StringNonNullCharacters(this System.Span span) { throw null; } public static string GetAsciiStringEscaped(this System.Span span, int maxChars) { throw null; } public static string GetAsciiStringNonNullCharacters(this System.Span span) { throw null; } + public static string GetHeaderName(this System.Span span) { throw null; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownHttpScheme(this System.Span span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpScheme knownScheme) { throw null; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownMethod(this System.Span span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, out int length) { throw null; } public static Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod GetKnownMethod(string value) { throw null; } diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 4b41f63c44..7ae053cb15 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -602,4 +602,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null. + + The request trailers are not available yet. They may not be available until the full request body is read. + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs index fdc8edb0cf..7c1327cd73 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs @@ -35,7 +35,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http : base(context) { RequestKeepAlive = keepAlive; - _requestBodyPipe = CreateRequestBodyPipe(context); } @@ -301,7 +300,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // _consumedBytes aren't tracked for trailer headers, since headers have separate limits. if (_mode == Mode.TrailerHeaders) { - if (_context.TakeMessageHeaders(readableBuffer, out consumed, out examined)) + if (_context.TakeMessageHeaders(readableBuffer, trailers: true, out consumed, out examined)) { _mode = Mode.Complete; } @@ -489,6 +488,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http consumed = trailerBuffer.End; AddAndCheckConsumedBytes(2); _mode = Mode.Complete; + // No trailers + _context.OnTrailersComplete(); } else { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index d657d70ee0..6a54f47e28 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http break; } case RequestProcessingStatus.ParsingHeaders: - if (TakeMessageHeaders(buffer, out consumed, out examined)) + if (TakeMessageHeaders(buffer, trailers: false, out consumed, out examined)) { _requestProcessingStatus = RequestProcessingStatus.AppStarted; } @@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return result; } - public bool TakeMessageHeaders(ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + public bool TakeMessageHeaders(ReadOnlySequence buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined) { // Make sure the buffer is limited bool overLength = false; @@ -202,7 +202,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http overLength = true; } - var result = _parser.ParseHeaders(new Http1ParsingHandler(this), buffer, out consumed, out examined, out var consumedBytes); + var result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), buffer, out consumed, out examined, out var consumedBytes); _remainingRequestHeadersBytesAllowed -= consumedBytes; if (!result && overLength) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs index 66d97819ba..2d13e68680 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs @@ -212,6 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _context.Input.AdvanceTo(consumed); _finalAdvanceCalled = true; + _context.OnTrailersComplete(); } return; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index a661946a62..0691c842d7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http BadHttpRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload); } + context.OnTrailersComplete(); // No trailers for these. return new Http1UpgradeMessageBody(context); } @@ -173,6 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http BadHttpRequestException.Throw(requestRejectionReason, context.Method); } + context.OnTrailersComplete(); // No trailers for these. return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs index bcc905cab9..b4c67c13a3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs @@ -8,17 +8,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http internal readonly struct Http1ParsingHandler : IHttpRequestLineHandler, IHttpHeadersHandler { public readonly Http1Connection Connection; + public readonly bool Trailers; public Http1ParsingHandler(Http1Connection connection) { Connection = connection; + Trailers = false; + } + + public Http1ParsingHandler(Http1Connection connection, bool trailers) + { + Connection = connection; + Trailers = trailers; } public void OnHeader(Span name, Span value) - => Connection.OnHeader(name, value); + { + if (Trailers) + { + Connection.OnTrailer(name, value); + } + else + { + Connection.OnHeader(name, value); + } + } public void OnHeadersComplete() - => Connection.OnHeadersComplete(); + { + if (Trailers) + { + Connection.OnTrailersComplete(); + } + else + { + Connection.OnHeadersComplete(); + } + } public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) => Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs index 637d6e086f..11fcadc074 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http IHttpConnectionFeature, IHttpRequestLifetimeFeature, IHttpRequestIdentifierFeature, + IHttpRequestTrailersFeature, IHttpBodyControlFeature, IHttpMaxRequestBodySizeFeature, IHttpResponseStartFeature, @@ -133,6 +134,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable; + + IHeaderDictionary IHttpRequestTrailersFeature.Trailers + { + get + { + if (!RequestTrailersAvailable) + { + throw new InvalidOperationException(CoreStrings.RequestTrailersNotAvailable); + } + return RequestTrailers; + } + } + int IHttpResponseFeature.StatusCode { get => StatusCode; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs index 8ba4e4b050..b9b3e26905 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly Type IRouteValuesFeatureType = typeof(IRouteValuesFeature); private static readonly Type IEndpointFeatureType = typeof(IEndpointFeature); private static readonly Type IHttpAuthenticationFeatureType = typeof(IHttpAuthenticationFeature); + private static readonly Type IHttpRequestTrailersFeatureType = typeof(IHttpRequestTrailersFeature); private static readonly Type IQueryFeatureType = typeof(IQueryFeature); private static readonly Type IFormFeatureType = typeof(IFormFeature); private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature); @@ -52,6 +53,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private object _currentIRouteValuesFeature; private object _currentIEndpointFeature; private object _currentIHttpAuthenticationFeature; + private object _currentIHttpRequestTrailersFeature; private object _currentIQueryFeature; private object _currentIFormFeature; private object _currentIHttpUpgradeFeature; @@ -82,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentIHttpUpgradeFeature = this; _currentIHttpRequestIdentifierFeature = this; _currentIHttpRequestLifetimeFeature = this; + _currentIHttpRequestTrailersFeature = this; _currentIHttpConnectionFeature = this; _currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpMinRequestBodyDataRateFeature = this; @@ -201,6 +204,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { feature = _currentIHttpAuthenticationFeature; } + else if (key == IHttpRequestTrailersFeatureType) + { + feature = _currentIHttpRequestTrailersFeature; + } else if (key == IQueryFeatureType) { feature = _currentIQueryFeature; @@ -321,6 +328,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _currentIHttpAuthenticationFeature = value; } + else if (key == IHttpRequestTrailersFeatureType) + { + _currentIHttpRequestTrailersFeature = value; + } else if (key == IQueryFeatureType) { _currentIQueryFeature = value; @@ -439,6 +450,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { feature = (TFeature)_currentIHttpAuthenticationFeature; } + else if (typeof(TFeature) == typeof(IHttpRequestTrailersFeature)) + { + feature = (TFeature)_currentIHttpRequestTrailersFeature; + } else if (typeof(TFeature) == typeof(IQueryFeature)) { feature = (TFeature)_currentIQueryFeature; @@ -563,6 +578,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _currentIHttpAuthenticationFeature = feature; } + else if (typeof(TFeature) == typeof(IHttpRequestTrailersFeature)) + { + _currentIHttpRequestTrailersFeature = feature; + } else if (typeof(TFeature) == typeof(IQueryFeature)) { _currentIQueryFeature = feature; @@ -679,6 +698,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { yield return new KeyValuePair(IHttpAuthenticationFeatureType, _currentIHttpAuthenticationFeature); } + if (_currentIHttpRequestTrailersFeature != null) + { + yield return new KeyValuePair(IHttpRequestTrailersFeatureType, _currentIHttpRequestTrailersFeature); + } if (_currentIQueryFeature != null) { yield return new KeyValuePair(IQueryFeatureType, _currentIQueryFeature); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 78701c5571..ea8e77cd3b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -206,6 +206,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } public IHeaderDictionary RequestHeaders { get; set; } + public IHeaderDictionary RequestTrailers { get; } = new HeaderDictionary(); + public bool RequestTrailersAvailable { get; set; } public Stream RequestBody { get; set; } public PipeReader RequestBodyPipeReader { get; set; } @@ -369,6 +371,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HttpResponseHeaders.Reset(); RequestHeaders = HttpRequestHeaders; ResponseHeaders = HttpResponseHeaders; + RequestTrailers.Clear(); + RequestTrailersAvailable = false; _isLeasedMemoryInvalid = true; _hasAdvanced = false; @@ -524,11 +528,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HttpRequestHeaders.Append(name, value); } + public void OnTrailer(Span name, Span value) + { + // Trailers still count towards the limit. + _requestHeadersParsed++; + if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) + { + BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + } + + string key = name.GetHeaderName(); + var valueStr = value.GetAsciiOrUTF8StringNonNullCharacters(); + RequestTrailers.Append(key, valueStr); + } + public void OnHeadersComplete() { HttpRequestHeaders.OnHeadersComplete(); } + public void OnTrailersComplete() + { + RequestTrailersAvailable = true; + } + public async Task ProcessRequestsAsync(IHttpApplication application) { try diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index ea3adc24ba..a6bf25ecbb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -103,16 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http [MethodImpl(MethodImplOptions.NoInlining)] private unsafe void AppendUnknownHeaders(Span name, string valueString) { - string key = new string('\0', name.Length); - fixed (byte* pKeyBytes = name) - fixed (char* keyBuffer = key) - { - if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, name.Length)) - { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName); - } - } - + string key = name.GetHeaderName(); Unknown.TryGetValue(key, out var existing); Unknown[key] = AppendValue(existing, valueString); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index fed15bdf83..fd5728b8e2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1073,9 +1073,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 ValidateHeader(name, value); try { - // Drop trailers for now. Adding them to the request headers is not thread safe. - // https://github.com/aspnet/KestrelHttpServer/issues/2051 - if (_requestHeaderParsingState != RequestHeaderParsingState.Trailers) + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + _currentHeadersStream.OnTrailer(name, value); + } + else { // Throws BadRequest for header count limit breaches. // Throws InvalidOperation for bad encoding. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index dfefd1a3d1..5a27a5641f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -403,6 +403,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + OnTrailersComplete(); RequestBodyPipe.Writer.Complete(); _inputFlowControl.StopWindowUpdates(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 7c5276259e..5c266d85df 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -84,6 +84,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } } + // The same as GetAsciiStringNonNullCharacters but throws BadRequest + public static unsafe string GetHeaderName(this Span span) + { + if (span.IsEmpty) + { + return string.Empty; + } + + var asciiString = new string('\0', span.Length); + + fixed (char* output = asciiString) + fixed (byte* buffer = span) + { + // This version if AsciiUtilities returns null if there are any null (0 byte) characters + // in the string + if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length)) + { + BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName); + } + } + + return asciiString; + } + public static unsafe string GetAsciiStringNonNullCharacters(this Span span) { if (span.IsEmpty) diff --git a/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs b/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs index a63a15c259..80f4cdecd6 100644 --- a/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs +++ b/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs @@ -2268,6 +2268,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2MinDataRateNotSupported() => GetString("Http2MinDataRateNotSupported"); + /// + /// The request trailers are not available yet. They may not be available until the full request body is read. + /// + internal static string RequestTrailersNotAvailable + { + get => GetString("RequestTrailersNotAvailable"); + } + + /// + /// The request trailers are not available yet. They may not be available until the full request body is read. + /// + internal static string FormatRequestTrailersNotAvailable() + => GetString("RequestTrailersNotAvailable"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs index 4c3cb758e9..494e4531e6 100644 --- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.UTF8.GetBytes("\r\n\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined); + _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(headerValue, _http1Connection.RequestHeaders[headerName]); @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(extendedAsciiEncoding.GetBytes("\r\n\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); } [Fact] @@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, exception.Message); @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_TooManyHeaders, exception.Message); @@ -250,7 +250,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined); + var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(takeMessageHeaders); @@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n")); readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined); + takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(takeMessageHeaders); diff --git a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs index c8fb802885..53f5cfcc1e 100644 --- a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs @@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpRequestIdentifierFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpRequestLifetimeFeature)] = CreateHttp1Connection(); + _collection[typeof(IHttpRequestTrailersFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpConnectionFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpMaxRequestBodySizeFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection(); @@ -144,6 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); + _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs index 75bc420343..84ac57e630 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs @@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ErrorUtilities.ThrowInvalidRequestLine(); } - if (!_http1Connection.TakeMessageHeaders(_buffer, out consumed, out examined)) + if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out consumed, out examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } @@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { _http1Connection.Reset(); - if (!_http1Connection.TakeMessageHeaders(_buffer, out var consumed, out var examined)) + if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out var consumed, out var examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs index 33ae90daeb..da9f0f7faf 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance readableBuffer = readableBuffer.Slice(consumed); - if (!Http1Connection.TakeMessageHeaders(readableBuffer, out consumed, out examined)) + if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } @@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance result = Pipe.Reader.ReadAsync().GetAwaiter().GetResult(); readableBuffer = result.Buffer; - if (!Http1Connection.TakeMessageHeaders(readableBuffer, out consumed, out examined)) + if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 7247e11226..2f3f01a941 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -232,18 +232,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var buffer = new byte[200]; + // The first request is chunked with no trailers. + if (requestsReceived == 0) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + } + // The last request is content-length with no trailers. + else + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); + } + while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) { ;// read to end } - if (requestsReceived < requestCount) + Assert.False(request.Headers.ContainsKey("X-Trailer-Header")); + + // The first request is chunked with no trailers. + if (requestsReceived == 0) { - Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString()); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString()); + } + // The last request is content-length with no trailers. else { - Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } requestsReceived++; @@ -278,6 +319,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests "POST / HTTP/1.1", "Host:", "Transfer-Encoding: chunked", + "Trailer: X-Trailer-Header", "", "C", $"HelloChunk{i:00}", @@ -315,6 +357,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var response = httpContext.Response; var request = httpContext.Request; + // The first request is chunked with no trailers. + if (requestsReceived == 0) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + } + // The last request is content-length with no trailers. + else + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); + } + while (true) { var result = await request.BodyReader.ReadAsync(); @@ -325,13 +390,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } - if (requestsReceived < requestCount) + Assert.False(request.Headers.ContainsKey("X-Trailer-Header")); + + // The first request is chunked with no trailers. + if (requestsReceived == 0) { - Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString()); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString()); + } + // The last request is content-length with no trailers. else { - Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } requestsReceived++; @@ -366,6 +449,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests "POST / HTTP/1.1", "Host:", "Transfer-Encoding: chunked", + "Trailer: X-Trailer-Header", "", "C", $"HelloChunk{i:00}", @@ -495,13 +579,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests ;// read to end } + Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + if (requestsReceived < requestCount) { - Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString()); - } - else - { - Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString()); } requestsReceived++; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 2081227840..593f3bc644 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1174,7 +1174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Theory] [InlineData(true)] [InlineData(false)] - public async Task HEADERS_Received_WithTrailers_Discarded(bool sendData) + public async Task HEADERS_Received_WithTrailers_Available(bool sendData) { await InitializeConnectionAsync(_readTrailersApplication); @@ -1206,10 +1206,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests VerifyDecodedRequestHeaders(_browserRequestHeaders); - // Make sure the trailers are missing. https://github.com/aspnet/KestrelHttpServer/issues/2630 + // Make sure the trailers are in the trailers collection. foreach (var header in _requestTrailers) { Assert.False(_receivedHeaders.ContainsKey(header.Key)); + Assert.True(_receivedTrailers.ContainsKey(header.Key)); + Assert.Equal(header.Value, _receivedTrailers[header.Key]); } await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); @@ -3288,7 +3290,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Theory] [InlineData(true)] [InlineData(false)] - public async Task CONTINUATION_Received_WithTrailers_Discarded(bool sendData) + public async Task CONTINUATION_Received_WithTrailers_Available(bool sendData) { await InitializeConnectionAsync(_readTrailersApplication); @@ -3331,9 +3333,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests VerifyDecodedRequestHeaders(_browserRequestHeaders); - // Make sure the trailers are missing. https://github.com/aspnet/KestrelHttpServer/issues/2630 + // Make sure the trailers are in the trailers collection. Assert.False(_receivedHeaders.ContainsKey("trailer-1")); Assert.False(_receivedHeaders.ContainsKey("trailer-2")); + Assert.True(_receivedTrailers.ContainsKey("trailer-1")); + Assert.True(_receivedTrailers.ContainsKey("trailer-2")); + Assert.Equal("1", _receivedTrailers["trailer-1"]); + Assert.Equal("2", _receivedTrailers["trailer-2"]); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 3e024a9bce..6751565db1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -130,6 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>(); protected readonly Dictionary _receivedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + protected readonly Dictionary _receivedTrailers = new Dictionary(StringComparer.OrdinalIgnoreCase); protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); protected readonly HashSet _abortedStreamIds = new HashSet(); protected readonly object _abortedStreamIdsLock = new object(); @@ -199,16 +200,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _readTrailersApplication = async context => { + Assert.True(context.Request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(context.Request.CheckTrailersAvailable(), "SupportsTrailers"); + using (var ms = new MemoryStream()) { // Consuming the entire request body guarantees trailers will be available await context.Request.Body.CopyToAsync(ms); } + Assert.True(context.Request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(context.Request.CheckTrailersAvailable(), "SupportsTrailers"); + foreach (var header in context.Request.Headers) { _receivedHeaders[header.Key] = header.Value.ToString(); } + + var trailers = context.Features.Get().Trailers; + + foreach (var header in trailers) + { + _receivedTrailers[header.Key] = header.Value.ToString(); + } }; _bufferingApplication = async context => diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs index 6feeeef83a..ef5e141063 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; @@ -360,7 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } while (count != 0); Assert.Equal("Hello World", Encoding.ASCII.GetString(buffer)); - Assert.Equal("trailing-value", context.Request.Headers["Trailing-Header"].ToString()); + Assert.Equal("trailing-value", context.Request.GetTrailer("Trailing-Header").ToString()); }, new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) { diff --git a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs index c8bd5f3d3e..8266770719 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs @@ -26,6 +26,7 @@ namespace CodeGenerator var commonFeatures = new[] { "IHttpAuthenticationFeature", + "IHttpRequestTrailersFeature", "IQueryFeature", "IFormFeature", }; @@ -69,6 +70,7 @@ namespace CodeGenerator "IHttpUpgradeFeature", "IHttpRequestIdentifierFeature", "IHttpRequestLifetimeFeature", + "IHttpRequestTrailersFeature", "IHttpConnectionFeature", "IHttpMaxRequestBodySizeFeature", "IHttpMinRequestBodyDataRateFeature",