diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .cs b/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .cs
new file mode 100644
index 0000000..09da208
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .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.
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ ///
+ /// Enum declaring the allowed values for the verbosity level when http.sys reject requests due to throttling.
+ ///
+ public enum Http503VerbosityLevel : long
+ {
+ ///
+ /// A 503 response is not sent; the connection is reset. This is the default HTTP Server API behavior.
+ ///
+ Basic = 0,
+
+ ///
+ /// The HTTP Server API sends a 503 response with a "Service Unavailable" reason phrase.
+ ///
+ Limited = 1,
+
+ ///
+ /// The HTTP Server API sends a 503 response with a detailed reason phrase.
+ ///
+ Full = 2
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
index cf34e6e..93d6e2d 100644
--- a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
@@ -2,18 +2,21 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Globalization;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.HttpSys
{
public class HttpSysOptions
{
+ private const Http503VerbosityLevel DefaultRejectionVerbosityLevel = Http503VerbosityLevel.Basic; // Http.sys default.
private const long DefaultRequestQueueLength = 1000; // Http.sys default.
internal static readonly int DefaultMaxAccepts = 5 * Environment.ProcessorCount;
// Matches the default maxAllowedContentLength in IIS (~28.6 MB)
// https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005
private const long DefaultMaxRequestBodySize = 30000000;
+ private Http503VerbosityLevel _rejectionVebosityLevel = DefaultRejectionVerbosityLevel;
// The native request queue
private long _requestQueueLength = DefaultRequestQueueLength;
private long? _maxConnections;
@@ -137,6 +140,38 @@ namespace Microsoft.AspNetCore.Server.HttpSys
///
public bool AllowSynchronousIO { get; set; } = true;
+ ///
+ /// Gets or sets a value that controls how http.sys reacts when rejecting requests due to throttling conditions - like when the request
+ /// queue limit is reached. The default in http.sys is "Basic" which means http.sys is just resetting the TCP connection. IIS uses Limited
+ /// as its default behavior which will result in sending back a 503 - Service Unavailable back to the client.
+ ///
+ public Http503VerbosityLevel Http503Verbosity
+ {
+ get
+ {
+ return _rejectionVebosityLevel;
+ }
+ set
+ {
+ if (value < Http503VerbosityLevel.Basic || value > Http503VerbosityLevel.Full)
+ {
+ string message = String.Format(
+ CultureInfo.InvariantCulture,
+ "The value must be one of the values defined in the '{0}' enum.",
+ typeof(Http503VerbosityLevel).Name);
+
+ throw new ArgumentOutOfRangeException(nameof(value), value, message);
+ }
+
+ if (_requestQueue != null)
+ {
+ _requestQueue.SetRejectionVerbosity(value);
+ }
+ // Only store it if it succeeds or hasn't started yet
+ _rejectionVebosityLevel = value;
+ }
+ }
+
internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue)
{
_urlGroup = urlGroup;
@@ -152,6 +187,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys
_requestQueue.SetLengthLimit(_requestQueueLength);
}
+ if (_rejectionVebosityLevel != DefaultRejectionVerbosityLevel)
+ {
+ _requestQueue.SetRejectionVerbosity(_rejectionVebosityLevel);
+ }
+
Authentication.SetUrlGroupSecurity(urlGroup);
Timeouts.SetUrlGroupTimeouts(urlGroup);
}
diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs
index 29f43b6..1d3546f 100644
--- a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs
@@ -99,6 +99,21 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
+ // The listener must be active for this to work.
+ internal unsafe void SetRejectionVerbosity(Http503VerbosityLevel verbosity)
+ {
+ CheckDisposed();
+
+ var result = HttpApi.HttpSetRequestQueueProperty(Handle,
+ HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServer503VerbosityProperty,
+ new IntPtr((void*)&verbosity), (uint)Marshal.SizeOf(), 0, IntPtr.Zero);
+
+ if (result != 0)
+ {
+ throw new HttpSysException((int)result);
+ }
+ }
+
public void Dispose()
{
if (_disposed)
diff --git a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs
index c15d47f..e70e3de 100644
--- a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs
+++ b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs
@@ -245,6 +245,22 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener
}
}
+ [ConditionalFact]
+ public async Task Server_SetRejectionVerbosityLevel_Success()
+ {
+ using (var server = Utilities.CreateHttpServer(out string address))
+ {
+ server.Options.Http503Verbosity = Http503VerbosityLevel.Limited;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
[ConditionalFact]
public async Task Server_HotAddPrefix_Success()
{
diff --git a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
index 4a451bd..e0ecbc7 100644
--- a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
+++ b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
@@ -298,6 +299,44 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
+ [ConditionalFact]
+ public async Task Server_SetHttp503VebosittHittingThrottle_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = 3;
+ server.Listener.Options.Http503Verbosity = Http503VerbosityLevel.Limited;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ {
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ using (HttpClient client4 = new HttpClient())
+ {
+ // Maxed out, refuses connection should return 503
+ HttpResponseMessage response = await client4.GetAsync(address);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+ }
+
+ // A connection has been closed, try again.
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+ }
+ }
+ }
+
[ConditionalFact]
public void Server_SetConnectionLimitArgumentValidation_Success()
{