diff --git a/eng/Baseline.props b/eng/Baseline.props index f052db5eb0..1d647402db 100644 --- a/eng/Baseline.props +++ b/eng/Baseline.props @@ -4,6 +4,24 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 2.2.0 + + + 2.2.0 + + + + + + + + + 2.2.0 + + + + + + 2.2.0 @@ -98,48 +116,48 @@ - 2.1.1 + 2.2.0 - - - + + + - 2.1.1 + 2.2.0 - - + + - 2.1.1 + 2.2.0 - + - + - 2.1.1 + 2.2.0 - - - - - - - - - - - + + + + + + + + + + + @@ -152,39 +170,39 @@ - 2.1.1 + 2.2.0 - + - 2.1.1 + 2.2.0 - - - + + + - 2.1.1 + 2.2.0 - + - 2.1.1 + 2.2.0 - - - - - + + + + + @@ -196,10 +214,10 @@ - 2.1.1 + 2.2.0 - + @@ -291,11 +309,11 @@ - 2.1.1 + 2.2.0 - - + + @@ -309,18 +327,18 @@ - 2.1.1 + 2.2.0 - + - 2.1.1 + 2.2.0 - + - + \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/TokenExtensions.cs b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs index 497acabc23..128b0651d1 100644 --- a/src/Http/Authentication.Abstractions/src/TokenExtensions.cs +++ b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs @@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Authentication /// Returns all of the AuthenticationTokens contained in the properties. /// /// The properties. - /// The authentication toekns. + /// The authentication tokens. public static IEnumerable GetTokens(this AuthenticationProperties properties) { if (properties == null) @@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Authentication /// The context. /// The name of the token. /// The value of the token. - public static Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) + public static Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName); /// diff --git a/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs index 050118d3c4..a7b913b1b2 100644 --- a/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs +++ b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -49,6 +50,9 @@ namespace Microsoft.AspNetCore.Authentication private readonly IDictionary _schemes; private readonly List _requestHandlers; + // Used as a safe return value for enumeration apis + private IEnumerable _schemesCopy = Array.Empty(); + private IEnumerable _requestHandlersCopy = Array.Empty(); private Task GetDefaultSchemeAsync() => _options.DefaultScheme != null @@ -102,7 +106,7 @@ namespace Microsoft.AspNetCore.Authentication /// /// Returns the scheme that will be used by default for . /// This is typically specified via . - /// Otherwise this will fallback to if that supoorts sign out. + /// Otherwise this will fallback to if that supports sign out. /// /// The scheme that will be used by default for . public virtual Task GetDefaultSignOutSchemeAsync() @@ -123,7 +127,7 @@ namespace Microsoft.AspNetCore.Authentication /// /// The schemes in priority order for request handling public virtual Task> GetRequestHandlerSchemesAsync() - => Task.FromResult>(_requestHandlers); + => Task.FromResult(_requestHandlersCopy); /// /// Registers a scheme for use by . @@ -144,8 +148,10 @@ namespace Microsoft.AspNetCore.Authentication if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) { _requestHandlers.Add(scheme); + _requestHandlersCopy = _requestHandlers.ToArray(); } _schemes[scheme.Name] = scheme; + _schemesCopy = _schemes.Values.ToArray(); } } @@ -164,13 +170,17 @@ namespace Microsoft.AspNetCore.Authentication if (_schemes.ContainsKey(name)) { var scheme = _schemes[name]; - _requestHandlers.Remove(scheme); + if (_requestHandlers.Remove(scheme)) + { + _requestHandlersCopy = _requestHandlers.ToArray(); + } _schemes.Remove(name); + _schemesCopy = _schemes.Values.ToArray(); } } } public virtual Task> GetAllSchemesAsync() - => Task.FromResult>(_schemes.Values); + => Task.FromResult(_schemesCopy); } } \ No newline at end of file diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index b9292ac1a8..392a441733 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -155,7 +155,7 @@ namespace Microsoft.Net.Http.Headers { if (!StringSegment.IsNullOrEmpty(fileName)) { - FileName = Sanatize(fileName); + FileName = Sanitize(fileName); } else { @@ -166,7 +166,7 @@ namespace Microsoft.Net.Http.Headers /// /// Sets the FileName parameter using encodings appropriate for MIME headers. - /// The FileNameStar paraemter is removed. + /// The FileNameStar parameter is removed. /// /// public void SetMimeFileName(StringSegment fileName) @@ -434,7 +434,7 @@ namespace Microsoft.Net.Http.Headers } // Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them. - private StringSegment Sanatize(StringSegment input) + private StringSegment Sanitize(StringSegment input) { var result = input; diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 20b4319252..c45ca9cf43 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -143,7 +143,7 @@ namespace Microsoft.Net.Http.Headers } } - // Since we never re-use a "found" value in 'y', we expecte 'alreadyFound' to have all fields set to 'true'. + // Since we never re-use a "found" value in 'y', we expected 'alreadyFound' to have all fields set to 'true'. // Otherwise the two collections can't be equal and we should not get here. Contract.Assert(Contract.ForAll(alreadyFound, value => { return value; }), "Expected all values in 'alreadyFound' to be true since collections are considered equal."); diff --git a/src/Http/Headers/src/HttpRuleParser.cs b/src/Http/Headers/src/HttpRuleParser.cs index 3741ffa110..05f4a4576f 100644 --- a/src/Http/Headers/src/HttpRuleParser.cs +++ b/src/Http/Headers/src/HttpRuleParser.cs @@ -225,7 +225,7 @@ namespace Microsoft.Net.Http.Headers return HttpParseResult.NotParsed; } - // Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char) + // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) // If so, check whether the character is in the range 0-127. If not, it's an invalid value. if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) { diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index 32074b44cc..e069210843 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValue.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -650,6 +650,7 @@ namespace Microsoft.Net.Http.Headers { return true; } + if (set.Suffix.HasValue) { if (Suffix.HasValue) @@ -663,7 +664,10 @@ namespace Microsoft.Net.Http.Headers } else { - return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase); + // If this subtype or suffix matches the subtype of the set, + // it is considered a subtype. + // Ex: application/json > application/val+json + return MatchesEitherSubtypeOrSuffix(set); } } @@ -673,6 +677,12 @@ namespace Microsoft.Net.Http.Headers set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); } + private bool MatchesEitherSubtypeOrSuffix(MediaTypeHeaderValue set) + { + return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase) || + set.SubType.Equals(Suffix, StringComparison.OrdinalIgnoreCase); + } + private bool MatchesParameters(MediaTypeHeaderValue set) { if (set._parameters != null && set._parameters.Count != 0) diff --git a/src/Http/Headers/src/RangeItemHeaderValue.cs b/src/Http/Headers/src/RangeItemHeaderValue.cs index 99fdbfef5c..3b177f6e9a 100644 --- a/src/Http/Headers/src/RangeItemHeaderValue.cs +++ b/src/Http/Headers/src/RangeItemHeaderValue.cs @@ -169,7 +169,7 @@ namespace Microsoft.Net.Http.Headers current = current + fromLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); - // Afer the first value, the '-' character must follow. + // After the first value, the '-' character must follow. if ((current == input.Length) || (input[current] != '-')) { // We need a '-' character otherwise this can't be a valid range. diff --git a/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs index ad1f7fce1f..f2c53a8727 100644 --- a/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs +++ b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs @@ -465,7 +465,7 @@ namespace Microsoft.Net.Http.Headers { { "inline", new ContentDispositionHeaderValue("inline") }, // @"This should be equivalent to not including the header at all." { "inline;", new ContentDispositionHeaderValue("inline") }, - { "inline;name=", new ContentDispositionHeaderValue("inline") { Parameters = { new NameValueHeaderValue("name", "") } } }, // TODO: passing in a null value causes a strange assert on CoreCLR before the test even starts. Not reproducable in the body of a test. + { "inline;name=", new ContentDispositionHeaderValue("inline") { Parameters = { new NameValueHeaderValue("name", "") } } }, // TODO: passing in a null value causes a strange assert on CoreCLR before the test even starts. Not reproducible in the body of a test. { "inline;name=value", new ContentDispositionHeaderValue("inline") { Name = "value" } }, { "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } }, { "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } }, diff --git a/src/Http/Headers/test/CookieHeaderValueTest.cs b/src/Http/Headers/test/CookieHeaderValueTest.cs index 416441991d..edd55bb7ab 100644 --- a/src/Http/Headers/test/CookieHeaderValueTest.cs +++ b/src/Http/Headers/test/CookieHeaderValueTest.cs @@ -286,7 +286,7 @@ namespace Microsoft.Net.Http.Headers public void CookieHeaderValue_ParseList_ExcludesInvalidValues(IList cookies, string[] input) { var results = CookieHeaderValue.ParseList(input); - // ParseList aways returns a list, even if empty. TryParseList may return null (via out). + // ParseList always returns a list, even if empty. TryParseList may return null (via out). Assert.Equal(cookies ?? new List(), results); } diff --git a/src/Http/Headers/test/EntityTagHeaderValueTest.cs b/src/Http/Headers/test/EntityTagHeaderValueTest.cs index f633fec226..b4ae186fb4 100644 --- a/src/Http/Headers/test/EntityTagHeaderValueTest.cs +++ b/src/Http/Headers/test/EntityTagHeaderValueTest.cs @@ -403,7 +403,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + public void ParseList_WithSomeInvalidValues_ExcludesInvalidValues() { var inputs = new[] { @@ -433,7 +433,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseStrictList_WithSomeInvlaidValues_Throws() + public void ParseStrictList_WithSomeInvalidValues_Throws() { var inputs = new[] { @@ -451,7 +451,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + public void TryParseList_WithSomeInvalidValues_ExcludesInvalidValues() { var inputs = new[] { @@ -482,7 +482,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() { var inputs = new[] { diff --git a/src/Http/Headers/test/MediaTypeHeaderValueTest.cs b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs index 75cccabc9c..63b2fa391b 100644 --- a/src/Http/Headers/test/MediaTypeHeaderValueTest.cs +++ b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs @@ -617,7 +617,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + public void ParseList_WithSomeInvalidValues_IgnoresInvalidValues() { var inputs = new[] { @@ -640,7 +640,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseStrictList_WithSomeInvlaidValues_Throws() + public void ParseStrictList_WithSomeInvalidValues_Throws() { var inputs = new[] { @@ -651,7 +651,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + public void TryParseList_WithSomeInvalidValues_IgnoresInvalidValues() { var inputs = new[] { @@ -676,7 +676,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() { var inputs = new[] { @@ -750,6 +750,8 @@ namespace Microsoft.Net.Http.Headers [InlineData("application/entity+json", "application/entity+json")] [InlineData("application/*+json", "application/entity+json")] [InlineData("application/*+json", "application/*+json")] + [InlineData("application/json", "application/problem+json")] + [InlineData("application/json", "application/vnd.restful+json")] [InlineData("application/*", "application/*+JSON")] [InlineData("application/vnd.github+json", "application/vnd.github+json")] [InlineData("application/*", "application/entity+JSON")] @@ -774,6 +776,7 @@ namespace Microsoft.Net.Http.Headers [InlineData("application/*+*", "application/json")] [InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards [InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards + [InlineData("application/entity+json", "application/entity")] public void IsSubSetOfWithSuffixes_NegativeCases(string set, string subset) { // Arrange diff --git a/src/Http/Headers/test/NameValueHeaderValueTest.cs b/src/Http/Headers/test/NameValueHeaderValueTest.cs index cac18debbb..4833b6898a 100644 --- a/src/Http/Headers/test/NameValueHeaderValueTest.cs +++ b/src/Http/Headers/test/NameValueHeaderValueTest.cs @@ -60,7 +60,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void Copy_NameOnly_SuccesfullyCopied() + public void Copy_NameOnly_SuccessfullyCopied() { var pair0 = new NameValueHeaderValue("name"); var pair1 = pair0.Copy(); @@ -95,7 +95,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void Copy_NameAndValue_SuccesfullyCopied() + public void Copy_NameAndValue_SuccessfullyCopied() { var pair0 = new NameValueHeaderValue("name", "value"); var pair1 = pair0.Copy(); @@ -466,7 +466,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + public void ParseList_WithSomeInvalidValues_ExcludesInvalidValues() { var inputs = new[] { @@ -502,7 +502,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseStrictList_WithSomeInvlaidValues_Throws() + public void ParseStrictList_WithSomeInvalidValues_Throws() { var inputs = new[] { @@ -520,7 +520,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseList_WithSomeInvlaidValues_ExcludesInvalidValues() + public void TryParseList_WithSomeInvalidValues_ExcludesInvalidValues() { var inputs = new[] { @@ -557,7 +557,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() { var inputs = new[] { diff --git a/src/Http/Headers/test/RangeConditionHeaderValueTest.cs b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs index ce7c73997b..dab3f670a4 100644 --- a/src/Http/Headers/test/RangeConditionHeaderValueTest.cs +++ b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs @@ -39,7 +39,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ToString_UseDifferentrangeConditions_AllSerializedCorrectly() + public void ToString_UseDifferentRangeConditions_AllSerializedCorrectly() { var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); Assert.Equal("\"x\"", rangeCondition.ToString()); @@ -49,7 +49,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void GetHashCode_UseSameAndDifferentrangeConditions_SameOrDifferentHashCodes() + public void GetHashCode_UseSameAndDifferentRangeConditions_SameOrDifferentHashCodes() { var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs index e7e8bf045a..9a920f40d0 100644 --- a/src/Http/Headers/test/SetCookieHeaderValueTest.cs +++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs @@ -389,7 +389,7 @@ namespace Microsoft.Net.Http.Headers public void SetCookieHeaderValue_ParseList_ExcludesInvalidValues(IList cookies, string[] input) { var results = SetCookieHeaderValue.ParseList(input); - // ParseList aways returns a list, even if empty. TryParseList may return null (via out). + // ParseList always returns a list, even if empty. TryParseList may return null (via out). Assert.Equal(cookies ?? new List(), results); } diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs index 49ee58b93e..971ad1028f 100644 --- a/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs +++ b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs @@ -354,7 +354,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + public void ParseList_WithSomeInvalidValues_IgnoresInvalidValues() { var inputs = new[] { @@ -392,7 +392,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void ParseStrictList_WithSomeInvlaidValues_Throws() + public void ParseStrictList_WithSomeInvalidValues_Throws() { var inputs = new[] { @@ -412,7 +412,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseList_WithSomeInvlaidValues_IgnoresInvalidValues() + public void TryParseList_WithSomeInvalidValues_IgnoresInvalidValues() { var inputs = new[] { @@ -451,7 +451,7 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse() + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() { var inputs = new[] { diff --git a/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs index a4f67ce4a2..4215b82697 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder.Extensions { /// - /// Respresents a middleware that maps a request path to a sub-request pipeline. + /// Represents a middleware that maps a request path to a sub-request pipeline. /// public class MapMiddleware { @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Builder.Extensions private readonly MapOptions _options; /// - /// Creates a new instace of . + /// Creates a new instance of . /// /// The delegate representing the next middleware in the request pipeline. /// The middleware options. diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs index b012626ba9..1441da4d99 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder.Extensions { /// - /// Respresents a middleware that runs a sub-request pipeline when a given predicate is matched. + /// Represents a middleware that runs a sub-request pipeline when a given predicate is matched. /// public class MapWhenMiddleware { diff --git a/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs new file mode 100644 index 0000000000..745cbfd9de --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs @@ -0,0 +1,53 @@ +// 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; + +namespace Microsoft.AspNetCore.Http +{ + public static class ResponseTrailerExtensions + { + private const string Trailer = "Trailer"; + + /// + /// Adds the given trailer name to the 'Trailer' response header. This must happen before the response headers are sent. + /// + /// + /// + public static void DeclareTrailer(this HttpResponse response, string trailerName) + { + response.Headers.AppendCommaSeparatedValues(Trailer, trailerName); + } + + /// + /// Indicates if the server supports sending trailer headers for this response. + /// + /// + /// + public static bool SupportsTrailers(this HttpResponse response) + { + var feature = response.HttpContext.Features.Get(); + return feature?.Trailers != null && !feature.Trailers.IsReadOnly; + } + + /// + /// Adds the given trailer header to the trailers collection to be sent at the end of the response body. + /// Check or an InvalidOperationException may be thrown. + /// + /// + /// + /// + public static void AppendTrailer(this HttpResponse response, string trailerName, StringValues trailerValues) + { + var feature = response.HttpContext.Features.Get(); + if (feature?.Trailers == null || feature.Trailers.IsReadOnly) + { + throw new InvalidOperationException("Trailers are not supported for this response."); + } + + feature.Trailers.Append(trailerName, trailerValues); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs index c07fe1e9f1..3342b4e08d 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs @@ -74,13 +74,13 @@ namespace Microsoft.AspNetCore.Builder throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); } - var methodinfo = invokeMethods[0]; - if (!typeof(Task).IsAssignableFrom(methodinfo.ReturnType)) + var methodInfo = invokeMethods[0]; + if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); } - var parameters = methodinfo.GetParameters(); + var parameters = methodInfo.GetParameters(); if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); @@ -92,10 +92,10 @@ namespace Microsoft.AspNetCore.Builder var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); if (parameters.Length == 1) { - return (RequestDelegate)methodinfo.CreateDelegate(typeof(RequestDelegate), instance); + return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); } - var factory = Compile(methodinfo, parameters); + var factory = Compile(methodInfo, parameters); return context => { @@ -142,13 +142,13 @@ namespace Microsoft.AspNetCore.Builder }); } - private static Func Compile(MethodInfo methodinfo, ParameterInfo[] parameters) + private static Func Compile(MethodInfo methodInfo, ParameterInfo[] parameters) { // If we call something like // // public class Middleware // { - // public Task Invoke(HttpContext context, ILoggerFactory loggeryFactory) + // public Task Invoke(HttpContext context, ILoggerFactory loggerFactory) // { // // } @@ -158,14 +158,14 @@ namespace Microsoft.AspNetCore.Builder // We'll end up with something like this: // Generic version: // - // Task Invoke(Middleware instance, HttpContext httpContext, IServiceprovider provider) + // Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider) // { // return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); // } // Non generic version: // - // Task Invoke(object instance, HttpContext httpContext, IServiceprovider provider) + // Task Invoke(object instance, HttpContext httpContext, IServiceProvider provider) // { // return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); // } @@ -190,7 +190,7 @@ namespace Microsoft.AspNetCore.Builder { providerArg, Expression.Constant(parameterType, typeof(Type)), - Expression.Constant(methodinfo.DeclaringType, typeof(Type)) + Expression.Constant(methodInfo.DeclaringType, typeof(Type)) }; var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); @@ -198,12 +198,12 @@ namespace Microsoft.AspNetCore.Builder } Expression middlewareInstanceArg = instanceArg; - if (methodinfo.DeclaringType != typeof(T)) + if (methodInfo.DeclaringType != typeof(T)) { - middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodinfo.DeclaringType); + middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType); } - var body = Expression.Call(middlewareInstanceArg, methodinfo, methodArguments); + var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments); var lambda = Expression.Lambda>(body, instanceArg, httpContextArg, providerArg); diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs index 6474aeda58..103bdd9601 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Builder.Extensions private readonly PathString _pathBase; /// - /// Creates a new instace of . + /// Creates a new instance of . /// /// The delegate representing the next middleware in the request pipeline. /// The path base to extract. diff --git a/src/Http/Http.Abstractions/src/HostString.cs b/src/Http/Http.Abstractions/src/HostString.cs index 9496b26bac..7851659be0 100644 --- a/src/Http/Http.Abstractions/src/HostString.cs +++ b/src/Http/Http.Abstractions/src/HostString.cs @@ -35,7 +35,12 @@ namespace Microsoft.AspNetCore.Http /// A positive, greater than 0 value representing the port in the host string. public HostString(string host, int port) { - if(port <= 0) + if (host == null) + { + throw new ArgumentNullException(nameof(host)); + } + + if (port <= 0) { throw new ArgumentOutOfRangeException(nameof(port), Resources.Exception_PortMustBeGreaterThanZero); } diff --git a/src/Http/Http.Abstractions/src/PathString.cs b/src/Http/Http.Abstractions/src/PathString.cs index 2a5960b661..b7f121a039 100644 --- a/src/Http/Http.Abstractions/src/PathString.cs +++ b/src/Http/Http.Abstractions/src/PathString.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Http private readonly string _value; /// - /// Initalize the path string with a given value. This value must be in unescaped format. Use + /// Initialize the path string with a given value. This value must be in unescaped format. Use /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. /// /// The unescaped path to be assigned to the Value property. @@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Http { if (!requiresEscaping) { - // the current segument doesn't require escape + // the current segment doesn't require escape if (buffer == null) { buffer = new StringBuilder(_value.Length * 3); diff --git a/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs index 0313a730d5..9274ab4207 100644 --- a/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs +++ b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Builder.Extensions public class MapPredicateMiddlewareTests { - private static readonly Predicate NotImplementedPredicate = new Predicate(envionment => { throw new NotImplementedException(); }); + private static readonly Predicate NotImplementedPredicate = new Predicate(environment => { throw new NotImplementedException(); }); private static Task Success(HttpContext context) { diff --git a/src/Http/Http.Abstractions/test/QueryStringTests.cs b/src/Http/Http.Abstractions/test/QueryStringTests.cs index 8327f12509..a78a853a1c 100644 --- a/src/Http/Http.Abstractions/test/QueryStringTests.cs +++ b/src/Http/Http.Abstractions/test/QueryStringTests.cs @@ -59,10 +59,10 @@ namespace Microsoft.AspNetCore.Http.Abstractions [InlineData("", "value", "?=value")] [InlineData("", "", "?=")] [InlineData("", null, "?=")] - public void CreateNameValue_Success(string name, string value, string exepcted) + public void CreateNameValue_Success(string name, string value, string expected) { var query = QueryString.Create(name, value); - Assert.Equal(exepcted, query.Value); + Assert.Equal(expected, query.Value); } [Fact] diff --git a/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs index 07c1aa4e8d..749309319f 100644 --- a/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs +++ b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs @@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public void UseMiddleware_MutlipleInvokeMethods_ThrowsException() + public void UseMiddleware_MultipleInvokeMethods_ThrowsException() { var builder = new ApplicationBuilder(new DummyServiceProvider()); builder.UseMiddleware(typeof(MiddlewareMultipleInvokesStub)); @@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public void UseMiddleware_MutlipleInvokeAsyncMethods_ThrowsException() + public void UseMiddleware_MultipleInvokeAsyncMethods_ThrowsException() { var builder = new ApplicationBuilder(new DummyServiceProvider()); builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAsyncStub)); @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public void UseMiddleware_MutlipleInvokeAndInvokeAsyncMethods_ThrowsException() + public void UseMiddleware_MultipleInvokeAndInvokeAsyncMethods_ThrowsException() { var builder = new ApplicationBuilder(new DummyServiceProvider()); builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAndInvokeAsyncStub)); @@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public void UseMiddlewareWithIvokeWithOutAndRefThrows() + public void UseMiddlewareWithInvokeWithOutAndRefThrows() { var mockServiceProvider = new DummyServiceProvider(); var builder = new ApplicationBuilder(mockServiceProvider); diff --git a/src/Http/Http.Features/src/FeatureReferences.cs b/src/Http/Http.Features/src/FeatureReferences.cs index 38bd2ec27a..04bc8d14be 100644 --- a/src/Http/Http.Features/src/FeatureReferences.cs +++ b/src/Http/Http.Features/src/FeatureReferences.cs @@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Http.Features } else if (flush) { - // Cache was cleared, but item retrived from current Collection for version + // Cache was cleared, but item retrieved from current Collection for version // so use passed in revision rather than making another virtual call Revision = revision; } diff --git a/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs b/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs new file mode 100644 index 0000000000..edd8fbea35 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpResponseTrailersFeature + { + IHeaderDictionary Trailers { get; set; } + } +} diff --git a/src/Http/Http/src/Features/FormFeature.cs b/src/Http/Http/src/Features/FormFeature.cs index f091e3b166..865e183f76 100644 --- a/src/Http/Http/src/Features/FormFeature.cs +++ b/src/Http/Http/src/Features/FormFeature.cs @@ -131,6 +131,11 @@ namespace Microsoft.AspNetCore.Http.Features cancellationToken.ThrowIfCancellationRequested(); + if (_request.ContentLength == 0) + { + return FormCollection.Empty; + } + if (_options.BufferBody) { _request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit); @@ -221,7 +226,7 @@ namespace Microsoft.AspNetCore.Http.Features // // value - // Do not limit the key name length here because the mulipart headers length limit is already in effect. + // Do not limit the key name length here because the multipart headers length limit is already in effect. var key = formDataSection.Name; var value = await formDataSection.GetValueAsync(); diff --git a/src/Http/Http/src/HttpContextAccessor.cs b/src/Http/Http/src/HttpContextAccessor.cs index 5a4676234c..897c27f734 100644 --- a/src/Http/Http/src/HttpContextAccessor.cs +++ b/src/Http/Http/src/HttpContextAccessor.cs @@ -7,17 +7,20 @@ namespace Microsoft.AspNetCore.Http { public class HttpContextAccessor : IHttpContextAccessor { - private static AsyncLocal _httpContextCurrent = new AsyncLocal(); + private static AsyncLocal<(string traceIdentifier, HttpContext context)> _httpContextCurrent = new AsyncLocal<(string traceIdentifier, HttpContext context)>(); public HttpContext HttpContext { get { - return _httpContextCurrent.Value; + var value = _httpContextCurrent.Value; + // Only return the context if the stored request id matches the stored trace identifier + // context.TraceIdentifier is cleared by HttpContextFactory.Dispose. + return value.traceIdentifier == value.context?.TraceIdentifier ? value.context : null; } set { - _httpContextCurrent.Value = value; + _httpContextCurrent.Value = (value?.TraceIdentifier, value); } } } diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs index 8236a388a5..f293ef4782 100644 --- a/src/Http/Http/src/HttpContextFactory.cs +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -53,6 +53,10 @@ namespace Microsoft.AspNetCore.Http { _httpContextAccessor.HttpContext = null; } + + // Null out the TraceIdentifier here as a sign that this request is done, + // the HttpContextAccessor implementation relies on this to detect that the request is over + httpContext.TraceIdentifier = null; } } } \ No newline at end of file diff --git a/src/Http/Http/test/DefaultHttpContextTests.cs b/src/Http/Http/test/DefaultHttpContextTests.cs index 33f73cf191..4aeeabb565 100644 --- a/src/Http/Http/test/DefaultHttpContextTests.cs +++ b/src/Http/Http/test/DefaultHttpContextTests.cs @@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Http features.Set(new HttpResponseFeature()); features.Set(new TestHttpWebSocketFeature()); - // featurecollection is set. all cached interfaces are null. + // FeatureCollection is set. all cached interfaces are null. var context = new DefaultHttpContext(features); TestAllCachedFeaturesAreNull(context, features); Assert.Equal(3, features.Count()); @@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http TestAllCachedFeaturesAreSet(context, features); Assert.NotEqual(3, features.Count()); - // featurecollection is null. and all cached interfaces are null. + // FeatureCollection is null. and all cached interfaces are null. // only top level is tested because child objects are inaccessible. context.Uninitialize(); TestCachedFeaturesAreNull(context, null); @@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Http newFeatures.Set(new HttpResponseFeature()); newFeatures.Set(new TestHttpWebSocketFeature()); - // featurecollection is set to newFeatures. all cached interfaces are null. + // FeatureCollection is set to newFeatures. all cached interfaces are null. context.Initialize(newFeatures); TestAllCachedFeaturesAreNull(context, newFeatures); Assert.Equal(3, newFeatures.Count()); diff --git a/src/Http/Http/test/Features/FormFeatureTests.cs b/src/Http/Http/test/Features/FormFeatureTests.cs index 591f46a43e..cfa8b0215b 100644 --- a/src/Http/Http/test/Features/FormFeatureTests.cs +++ b/src/Http/Http/test/Features/FormFeatureTests.cs @@ -12,6 +12,23 @@ namespace Microsoft.AspNetCore.Http.Features { public class FormFeatureTests { + [Fact] + public async Task ReadFormAsync_0ContentLength_ReturnsEmptyForm() + { + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.ContentLength = 0; + + var formFeature = new FormFeature(context.Request, new FormOptions()); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.Same(FormCollection.Empty, formCollection); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Http/Http/test/HeaderDictionaryTests.cs b/src/Http/Http/test/HeaderDictionaryTests.cs index 03d642a018..26113f53c0 100644 --- a/src/Http/Http/test/HeaderDictionaryTests.cs +++ b/src/Http/Http/test/HeaderDictionaryTests.cs @@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public void EmtpyQuotedHeaderSegmentsAreIgnored() + public void EmptyQuotedHeaderSegmentsAreIgnored() { var headers = new HeaderDictionary( new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/src/Http/Http/test/HttpContextAccessorTests.cs b/src/Http/Http/test/HttpContextAccessorTests.cs new file mode 100644 index 0000000000..c1521b1bc3 --- /dev/null +++ b/src/Http/Http/test/HttpContextAccessorTests.cs @@ -0,0 +1,197 @@ +// 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 System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Reflection; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextAccessorTests + { + [Fact] + public async Task HttpContextAccessor_GettingHttpContextReturnsHttpContext() + { + var accessor = new HttpContextAccessor(); + + var context = new DefaultHttpContext(); + context.TraceIdentifier = "1"; + accessor.HttpContext = context; + + await Task.Delay(100); + + Assert.Same(context, accessor.HttpContext); + } + + [Fact] + public void HttpContextAccessor_GettingHttpContextWithOutSettingReturnsNull() + { + var accessor = new HttpContextAccessor(); + + Assert.Null(accessor.HttpContext); + } + + [Fact] + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfSetToNull() + { + var accessor = new HttpContextAccessor(); + + var context = new DefaultHttpContext(); + context.TraceIdentifier = "1"; + accessor.HttpContext = context; + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ThreadPool.QueueUserWorkItem(async _ => + { + // The HttpContext flows with the execution context + Assert.Same(context, accessor.HttpContext); + + checkAsyncFlowTcs.SetResult(null); + + await waitForNullTcs.Task; + + try + { + Assert.Null(accessor.HttpContext); + + afterNullCheckTcs.SetResult(null); + } + catch (Exception ex) + { + afterNullCheckTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + + // Null out the accessor + accessor.HttpContext = null; + context.TraceIdentifier = null; + + waitForNullTcs.SetResult(null); + + Assert.Null(accessor.HttpContext); + + await afterNullCheckTcs.Task; + } + + [Fact] + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfDifferentTraceIdentifier() + { + var accessor = new HttpContextAccessor(); + + var context = new DefaultHttpContext(); + context.TraceIdentifier = "1"; + accessor.HttpContext = context; + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ThreadPool.QueueUserWorkItem(async _ => + { + // The HttpContext flows with the execution context + Assert.Same(context, accessor.HttpContext); + + checkAsyncFlowTcs.SetResult(null); + + await waitForNullTcs.Task; + + try + { + Assert.Null(accessor.HttpContext); + + afterNullCheckTcs.SetResult(null); + } + catch (Exception ex) + { + afterNullCheckTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + + // Reset the trace identifier on the first request + context.TraceIdentifier = null; + + // Set a new http context + var context2 = new DefaultHttpContext(); + context2.TraceIdentifier = "2"; + accessor.HttpContext = context2; + + waitForNullTcs.SetResult(null); + + Assert.Same(context2, accessor.HttpContext); + + await afterNullCheckTcs.Task; + } + + [Fact] + public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfAccessorSetToNull() + { + var accessor = new HttpContextAccessor(); + + var context = new DefaultHttpContext(); + context.TraceIdentifier = "1"; + accessor.HttpContext = context; + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + accessor.HttpContext = null; + + ThreadPool.QueueUserWorkItem(_ => + { + try + { + // The HttpContext flows with the execution context + Assert.Null(accessor.HttpContext); + checkAsyncFlowTcs.SetResult(null); + } + catch (Exception ex) + { + checkAsyncFlowTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + } + + [Fact] + public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfExecutionContextDoesNotFlow() + { + var accessor = new HttpContextAccessor(); + + var context = new DefaultHttpContext(); + context.TraceIdentifier = "1"; + accessor.HttpContext = context; + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + try + { + // The HttpContext flows with the execution context + Assert.Null(accessor.HttpContext); + checkAsyncFlowTcs.SetResult(null); + } + catch (Exception ex) + { + checkAsyncFlowTcs.SetException(ex); + } + }, null); + + await checkAsyncFlowTcs.Task; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/HttpContextFactoryTests.cs b/src/Http/Http/test/HttpContextFactoryTests.cs index ba983198e7..80e421273a 100644 --- a/src/Http/Http/test/HttpContextFactoryTests.cs +++ b/src/Http/Http/test/HttpContextFactoryTests.cs @@ -22,7 +22,27 @@ namespace Microsoft.AspNetCore.Http var context = contextFactory.Create(new FeatureCollection()); // Assert - Assert.True(ReferenceEquals(context, accessor.HttpContext)); + Assert.Same(context, accessor.HttpContext); + } + + [Fact] + public void DisposeHttpContextSetsHttpContextAccessorToNull() + { + // Arrange + var accessor = new HttpContextAccessor(); + var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), accessor); + + // Act + var context = contextFactory.Create(new FeatureCollection()); + var traceIdentifier = context.TraceIdentifier; + + // Assert + Assert.Same(context, accessor.HttpContext); + + contextFactory.Dispose(context); + + Assert.Null(accessor.HttpContext); + Assert.NotEqual(traceIdentifier, context.TraceIdentifier); } [Fact] diff --git a/src/Http/Http/test/ResponseCookiesTest.cs b/src/Http/Http/test/ResponseCookiesTest.cs index 5e5c44f89d..a6aa2de5ba 100644 --- a/src/Http/Http/test/ResponseCookiesTest.cs +++ b/src/Http/Http/test/ResponseCookiesTest.cs @@ -15,13 +15,13 @@ namespace Microsoft.AspNetCore.Http.Tests { var headers = new HeaderDictionary(); var cookies = new ResponseCookies(headers, null); - var testcookie = "TestCookie"; + var testCookie = "TestCookie"; - cookies.Delete(testcookie); + cookies.Delete(testCookie); var cookieHeaderValues = headers[HeaderNames.SetCookie]; Assert.Single(cookieHeaderValues); - Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); Assert.Contains("path=/", cookieHeaderValues[0]); Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); } @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Http.Tests { var headers = new HeaderDictionary(); var cookies = new ResponseCookies(headers, null); - var testcookie = "TestCookie"; + var testCookie = "TestCookie"; var time = new DateTimeOffset(2000, 1, 1, 1, 1, 1, 1, TimeSpan.Zero); var options = new CookieOptions { @@ -43,11 +43,11 @@ namespace Microsoft.AspNetCore.Http.Tests SameSite = SameSiteMode.Lax }; - cookies.Delete(testcookie, options); + cookies.Delete(testCookie, options); var cookieHeaderValues = headers[HeaderNames.SetCookie]; Assert.Single(cookieHeaderValues); - Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); Assert.Contains("path=/", cookieHeaderValues[0]); Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); Assert.Contains("secure", cookieHeaderValues[0]); @@ -60,14 +60,14 @@ namespace Microsoft.AspNetCore.Http.Tests { var headers = new HeaderDictionary(); var cookies = new ResponseCookies(headers, null); - var testcookie = "TestCookie"; + var testCookie = "TestCookie"; - cookies.Append(testcookie, testcookie); - cookies.Delete(testcookie); + cookies.Append(testCookie, testCookie); + cookies.Delete(testCookie); var cookieHeaderValues = headers[HeaderNames.SetCookie]; Assert.Single(cookieHeaderValues); - Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); Assert.Contains("path=/", cookieHeaderValues[0]); Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); } @@ -80,9 +80,9 @@ namespace Microsoft.AspNetCore.Http.Tests var cookieOptions = new CookieOptions(); var maxAgeTime = TimeSpan.FromHours(1); cookieOptions.MaxAge = TimeSpan.FromHours(1); - var testcookie = "TestCookie"; + var testCookie = "TestCookie"; - cookies.Append(testcookie, testcookie, cookieOptions); + cookies.Append(testCookie, testCookie, cookieOptions); var cookieHeaderValues = headers[HeaderNames.SetCookie]; Assert.Single(cookieHeaderValues); diff --git a/src/Http/Owin/src/OwinExtensions.cs b/src/Http/Owin/src/OwinExtensions.cs index 0344c1a552..b7fcf15d04 100644 --- a/src/Http/Owin/src/OwinExtensions.cs +++ b/src/Http/Owin/src/OwinExtensions.cs @@ -34,11 +34,11 @@ namespace Microsoft.AspNetCore.Builder { Func middleware1 = next1 => { - AppFunc exitMiddlware = env => + AppFunc exitMiddleware = env => { return next1((HttpContext)env[typeof(HttpContext).FullName]); }; - var app = middleware(exitMiddlware); + var app = middleware(exitMiddleware); return httpContext => { // Use the existing OWIN env if there is one. diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs index 5fe43dedd2..074da1f968 100644 --- a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs @@ -103,10 +103,10 @@ namespace Microsoft.AspNetCore.Owin // 2. The middleware inserts an alternate Accept signature into the OWIN environment. // 3. The middleware invokes Next and stores Next's Task locally. It then returns an alternate Task to the server. // 4. The OwinFeatureCollection adapts the alternate Accept signature to IHttpWebSocketFeature.AcceptAsync. - // 5. A component later in the pipleline invokes IHttpWebSocketFeature.AcceptAsync (mapped to AcceptWebSocketAsync). + // 5. A component later in the pipeline invokes IHttpWebSocketFeature.AcceptAsync (mapped to AcceptWebSocketAsync). // 6. The middleware calls the OWIN Accept, providing a local callback, and returns an incomplete Task. // 7. The middleware completes the alternate Task it returned from Invoke, telling the server that the request pipeline has completed. - // 8. The server invokes the middleware's callback, which creats a WebSocket adapter complete's the orriginal Accept Task with it. + // 8. The server invokes the middleware's callback, which creates a WebSocket adapter and completes the original Accept Task with it. // 9. The middleware waits while the application uses the WebSocket, where the end is signaled by the Next's Task completion. public static AppFunc AdaptWebSockets(AppFunc next) { diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs index b728802914..58538c7f84 100644 --- a/src/Http/Owin/test/OwinEnvironmentTests.cs +++ b/src/Http/Owin/test/OwinEnvironmentTests.cs @@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Owin } [Fact] - public void OwinEnvironmentImpelmentsGetEnumerator() + public void OwinEnvironmentImplementsGetEnumerator() { var owinEnvironment = new OwinEnvironment(CreateContext()); diff --git a/src/Http/WebUtilities/src/FormReader.cs b/src/Http/WebUtilities/src/FormReader.cs index 958a4971fa..65bfc37be4 100644 --- a/src/Http/WebUtilities/src/FormReader.cs +++ b/src/Http/WebUtilities/src/FormReader.cs @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.WebUtilities public KeyValuePair? ReadNextPair() { ReadNextPairImpl(); - if (ReadSucceded()) + if (ReadSucceeded()) { return new KeyValuePair(_currentKey, _currentValue); } @@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.WebUtilities public async Task?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) { await ReadNextPairAsyncImpl(cancellationToken); - if (ReadSucceded()) + if (ReadSucceeded()) { return new KeyValuePair(_currentKey, _currentValue); } @@ -189,11 +189,11 @@ namespace Microsoft.AspNetCore.WebUtilities return true; } - private bool TryReadWord(char seperator, int limit, out string value) + private bool TryReadWord(char separator, int limit, out string value) { do { - if (ReadChar(seperator, limit, out value)) + if (ReadChar(separator, limit, out value)) { return true; } @@ -201,7 +201,7 @@ namespace Microsoft.AspNetCore.WebUtilities return false; } - private bool ReadChar(char seperator, int limit, out string word) + private bool ReadChar(char separator, int limit, out string word) { // End if (_bufferCount == 0) @@ -213,7 +213,7 @@ namespace Microsoft.AspNetCore.WebUtilities var c = _buffer[_bufferOffset++]; _bufferCount--; - if (c == seperator) + if (c == separator) { word = BuildWord(); return true; @@ -283,14 +283,14 @@ namespace Microsoft.AspNetCore.WebUtilities return accumulator.GetResults(); } - private bool ReadSucceded() + private bool ReadSucceeded() { return _currentKey != null && _currentValue != null; } private void Append(ref KeyValueAccumulator accumulator) { - if (ReadSucceded()) + if (ReadSucceeded()) { accumulator.Append(_currentKey, _currentValue); if (accumulator.ValueCount > ValueCountLimit) diff --git a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs index 050088ccb7..9e0bf57c92 100644 --- a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs +++ b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -150,34 +152,66 @@ namespace Microsoft.AspNetCore.WebUtilities } } - public override async Task WriteAsync(char value) + public override Task WriteAsync(char value) { if (_disposed) { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + return GetObjectDisposedTask(); } if (_charBufferCount == _charBufferSize) { - await FlushInternalAsync(flushEncoder: false); + return WriteAsyncAwaited(value); } + else + { + // Enough room in buffer, no need to go async + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + return Task.CompletedTask; + } + } + + private async Task WriteAsyncAwaited(char value) + { + Debug.Assert(_charBufferCount == _charBufferSize); + + await FlushInternalAsync(flushEncoder: false); _charBuffer[_charBufferCount] = value; _charBufferCount++; } - public override async Task WriteAsync(char[] values, int index, int count) + public override Task WriteAsync(char[] values, int index, int count) { if (_disposed) { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + return GetObjectDisposedTask(); } - if (values == null) + if (values == null || count == 0) { - return; + return Task.CompletedTask; } + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= count) + { + // Enough room in buffer, no need to go async + CopyToCharBuffer(values, ref index, ref count); + return Task.CompletedTask; + } + else + { + return WriteAsyncAwaited(values, index, count); + } + } + + private async Task WriteAsyncAwaited(char[] values, int index, int count) + { + Debug.Assert(count > 0); + Debug.Assert(_charBufferSize - _charBufferCount > count); + while (count > 0) { if (_charBufferCount == _charBufferSize) @@ -186,22 +220,43 @@ namespace Microsoft.AspNetCore.WebUtilities } CopyToCharBuffer(values, ref index, ref count); + Debug.Assert(count == 0); } } - public override async Task WriteAsync(string value) + public override Task WriteAsync(string value) { if (_disposed) { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + return GetObjectDisposedTask(); } - if (value == null) + var count = value?.Length ?? 0; + if (count == 0) { - return; + return Task.CompletedTask; } + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= count) + { + // Enough room in buffer, no need to go async + CopyToCharBuffer(value); + return Task.CompletedTask; + } + else + { + return WriteAsyncAwaited(value); + } + } + + private async Task WriteAsyncAwaited(string value) + { var count = value.Length; + + Debug.Assert(count > 0); + Debug.Assert(_charBufferSize - _charBufferCount < count); + var index = 0; while (count > 0) { @@ -231,7 +286,7 @@ namespace Microsoft.AspNetCore.WebUtilities { if (_disposed) { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + return GetObjectDisposedTask(); } return FlushInternalAsync(flushEncoder: true); @@ -306,6 +361,19 @@ namespace Microsoft.AspNetCore.WebUtilities } } + private void CopyToCharBuffer(string value) + { + Debug.Assert(_charBufferSize - _charBufferCount >= value.Length); + + value.CopyTo( + sourceIndex: 0, + destination: _charBuffer, + destinationIndex: _charBufferCount, + count: value.Length); + + _charBufferCount += value.Length; + } + private void CopyToCharBuffer(string value, ref int index, ref int count) { var remaining = Math.Min(_charBufferSize - _charBufferCount, count); @@ -336,5 +404,11 @@ namespace Microsoft.AspNetCore.WebUtilities index += remaining; count -= remaining; } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Task GetObjectDisposedTask() + { + return Task.FromException(new ObjectDisposedException(nameof(HttpResponseStreamWriter))); + } } } diff --git a/src/Http/WebUtilities/src/QueryHelpers.cs b/src/Http/WebUtilities/src/QueryHelpers.cs index 6bd1a0bb82..c1c23b64e9 100644 --- a/src/Http/WebUtilities/src/QueryHelpers.cs +++ b/src/Http/WebUtilities/src/QueryHelpers.cs @@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.WebUtilities var anchorIndex = uri.IndexOf('#'); var uriToBeAppended = uri; var anchorText = ""; - // If there is an anchor, then the query string must be inserted before its first occurance. + // If there is an anchor, then the query string must be inserted before its first occurence. if (anchorIndex != -1) { anchorText = uri.Substring(anchorIndex); diff --git a/src/Http/WebUtilities/test/MultipartReaderTests.cs b/src/Http/WebUtilities/test/MultipartReaderTests.cs index d66ea98fed..b92bb5ff92 100644 --- a/src/Http/WebUtilities/test/MultipartReaderTests.cs +++ b/src/Http/WebUtilities/test/MultipartReaderTests.cs @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadSinglePartBody_Success() + public async Task MultipartReader_ReadSinglePartBody_Success() { var stream = MakeStream(OnePartBody); var reader = new MultipartReader(Boundary, stream); @@ -123,7 +123,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_HeaderCountExceeded_Throws() + public async Task MultipartReader_HeaderCountExceeded_Throws() { var stream = MakeStream(OnePartBodyTwoHeaders); var reader = new MultipartReader(Boundary, stream) @@ -136,7 +136,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_HeadersLengthExceeded_Throws() + public async Task MultipartReader_HeadersLengthExceeded_Throws() { var stream = MakeStream(OnePartBodyTwoHeaders); var reader = new MultipartReader(Boundary, stream) @@ -149,7 +149,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() + public async Task MultipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() { var stream = MakeStream(OnePartBodyWithTrailingWhitespace); var reader = new MultipartReader(Boundary, stream); @@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() + public async Task MultipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() { var stream = MakeStream(OnePartBodyWithoutFinalCRLF); var reader = new MultipartReader(Boundary, stream); @@ -183,7 +183,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadTwoPartBody_Success() + public async Task MultipartReader_ReadTwoPartBody_Success() { var stream = MakeStream(TwoPartBody); var reader = new MultipartReader(Boundary, stream); @@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + public async Task MultipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() { var stream = MakeStream(TwoPartBodyWithUnicodeFileName); var reader = new MultipartReader(Boundary, stream); @@ -235,7 +235,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ThreePartBody_Success() + public async Task MultipartReader_ThreePartBody_Success() { var stream = MakeStream(ThreePartBody); var reader = new MultipartReader(Boundary, stream); @@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public void MutipartReader_BufferSizeMustBeLargerThanBoundary_Throws() + public void MultipartReader_BufferSizeMustBeLargerThanBoundary_Throws() { var stream = MakeStream(ThreePartBody); Assert.Throws(() => @@ -280,7 +280,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() + public async Task MultipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() { var stream = MakeStream(TwoPartBodyIncompleteBuffer); var reader = new MultipartReader(Boundary, stream); @@ -311,7 +311,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadInvalidUtf8Header_ReplacementCharacters() + public async Task MultipartReader_ReadInvalidUtf8Header_ReplacementCharacters() { var body1 = "--9051914041544843365972754266\r\n" + @@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.WebUtilities } [Fact] - public async Task MutipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() + public async Task MultipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() { var body1 = "--9051914041544843365972754266\r\n" + diff --git a/src/Http/samples/SampleApp/SampleApp.csproj b/src/Http/samples/SampleApp/SampleApp.csproj index aedd176bec..6bccd19d19 100644 --- a/src/Http/samples/SampleApp/SampleApp.csproj +++ b/src/Http/samples/SampleApp/SampleApp.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;net461 + netcoreapp2.2;net461 Exe