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",