Response buffering in IIS is disabled by default (#1040)

Disables write buffering of response in IIS when sending it back to the client. Having buffering enabled breaks Server Side Events scenario. Buffering can be enabled again via the new config parameter `RequestProxyConfig.AllowResponseBuffering`.

Fixes #533
This commit is contained in:
Alexander Nikolaev 2021-06-04 11:19:06 +02:00 коммит произвёл GitHub
Родитель c14613da75
Коммит d2c7863f28
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 67 добавлений и 8 удалений

Просмотреть файл

@ -81,7 +81,8 @@ HTTP request configuration is based on [RequestProxyConfig](xref:Yarp.ReversePro
"HttpRequest": {
"Timeout": "<timespan>",
"Version": "<string>",
"VersionPolicy": ["RequestVersionOrLower", "RequestVersionOrHigher", "RequestVersionExact"]
"VersionPolicy": ["RequestVersionOrLower", "RequestVersionOrHigher", "RequestVersionExact"],
"AllowResponseBuffering": "<bool>"
}
```
@ -89,6 +90,7 @@ Configuration settings:
- Timeout - the timeout for the outgoing request sent by [HttpMessageInvoker.SendAsync](https://docs.microsoft.com/dotnet/api/system.net.http.httpmessageinvoker.sendasync). If not specified, 100 seconds is used.
- Version - outgoing request [version](https://docs.microsoft.com/dotnet/api/system.net.http.httprequestmessage.version). The supported values at the moment are `1.0`, `1.1` and `2`. Default value is 2.
- VersionPolicy - defines how the final version is selected for the outgoing requests. This feature is available from .NET 5.0, see [HttpRequestMessage.VersionPolicy](https://docs.microsoft.com/dotnet/api/system.net.http.httprequestmessage.versionpolicy). The default value is `RequestVersionOrLower`.
- AllowResponseBuffering - allows to use write buffering when sending a response back to the client, if the server hosting YARP (e.g. IIS) supports it. **NOTE**: enabling it can break SSE (server side event) scenarios.
## Configuration example

Просмотреть файл

@ -340,6 +340,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider
#if NET
VersionPolicy = section.ReadEnum<HttpVersionPolicy>(nameof(RequestProxyConfig.VersionPolicy)),
#endif
AllowResponseBuffering = section.ReadBool(nameof(RequestProxyConfig.AllowResponseBuffering))
};
}

Просмотреть файл

@ -303,6 +303,11 @@ namespace Yarp.ReverseProxy.Proxy
Log.Proxying(_logger, destinationRequest.RequestUri);
if (requestConfig?.AllowResponseBuffering != true)
{
context.Features.Get<IHttpResponseBodyFeature>()?.DisableBuffering();
}
// TODO: What if they replace the HttpContent object? That would mess with our tracking and error handling.
return (destinationRequest, requestContent);
}

Просмотреть файл

@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Yarp.ReverseProxy.Model;
using Yarp.ReverseProxy.Utilities;

Просмотреть файл

@ -36,6 +36,13 @@ namespace Yarp.ReverseProxy.Proxy
public HttpVersionPolicy? VersionPolicy { get; init; }
#endif
/// <summary>
/// Allows to use write buffering when sending a response back to the client,
/// if the server hosting YARP (e.g. IIS) supports it.
/// NOTE: enabling it can break SSE (server side event) scenarios.
/// </summary>
public bool? AllowResponseBuffering { get; init; }
/// <inheritdoc />
public bool Equals(RequestProxyConfig? other)
{
@ -48,7 +55,8 @@ namespace Yarp.ReverseProxy.Proxy
#if NET
&& VersionPolicy == other.VersionPolicy
#endif
&& Version == other.Version;
&& Version == other.Version
&& AllowResponseBuffering == other.AllowResponseBuffering;
}
/// <inheritdoc />
@ -58,7 +66,8 @@ namespace Yarp.ReverseProxy.Proxy
#if NET
VersionPolicy,
#endif
Version);
Version,
AllowResponseBuffering);
}
}
}

Просмотреть файл

@ -68,7 +68,8 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
Timeout = TimeSpan.FromSeconds(6),
Policy = "Any5xxResponse",
Path = "healthCheckPath"
}
},
AvailableDestinationsPolicy = "HealthyOrPanic"
},
LoadBalancingPolicy = LoadBalancingPolicies.Random,
SessionAffinity = new SessionAffinityConfig
@ -106,6 +107,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
#if NET
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
#endif
AllowResponseBuffering = true
},
Metadata = new Dictionary<string, string> { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } }
}
@ -234,7 +236,8 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
""HttpRequest"": {
""Timeout"": ""00:01:00"",
""Version"": ""1"",
""VersionPolicy"": ""RequestVersionExact""
""VersionPolicy"": ""RequestVersionExact"",
""AllowResponseBuffering"": ""true""
},
""Destinations"": {
""destinationA"": {
@ -472,6 +475,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
Assert.Equal(cluster1.Destinations["destinationB"].Address, abstractCluster1.Destinations["destinationB"].Address);
Assert.Equal(cluster1.Destinations["destinationB"].Health, abstractCluster1.Destinations["destinationB"].Health);
Assert.Equal(cluster1.Destinations["destinationB"].Metadata, abstractCluster1.Destinations["destinationB"].Metadata);
Assert.Equal(cluster1.HealthCheck.AvailableDestinationsPolicy, abstractCluster1.HealthCheck.AvailableDestinationsPolicy);
Assert.Equal(cluster1.HealthCheck.Passive.Enabled, abstractCluster1.HealthCheck.Passive.Enabled);
Assert.Equal(cluster1.HealthCheck.Passive.Policy, abstractCluster1.HealthCheck.Passive.Policy);
Assert.Equal(cluster1.HealthCheck.Passive.ReactivationPeriod, abstractCluster1.HealthCheck.Passive.ReactivationPeriod);
@ -505,6 +509,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
#if NET
Assert.Equal(cluster1.HttpRequest.VersionPolicy, abstractCluster1.HttpRequest.VersionPolicy);
#endif
Assert.Equal(cluster1.HttpRequest.AllowResponseBuffering, abstractCluster1.HttpRequest.AllowResponseBuffering);
Assert.Equal(cluster1.HttpClient.DangerousAcceptAnyServerCertificate, abstractCluster1.HttpClient.DangerousAcceptAnyServerCertificate);
Assert.Equal(cluster1.Metadata, abstractCluster1.Metadata);

Просмотреть файл

@ -1381,6 +1381,39 @@ namespace Yarp.ReverseProxy.Proxy.Tests
events.AssertContainProxyStages(hasRequestContent: false);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
[InlineData(null)]
public async Task ProxyAsync_ResponseBodyDisableBuffering_Success(bool? enableBuffering)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
var responseBody = new TestResponseBody();
httpContext.Features.Set<IHttpResponseFeature>(responseBody);
httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);
httpContext.Features.Set<IHttpRequestLifetimeFeature>(responseBody);
var destinationPrefix = "https://localhost:123/";
var sut = CreateProxy();
var client = MockHttpHandler.CreateClient(
(HttpRequestMessage request, CancellationToken cancellationToken) =>
{
var message = new HttpResponseMessage()
{
Content = new StreamContent(new MemoryStream(new byte[1]))
};
message.Headers.AcceptRanges.Add("bytes");
return Task.FromResult(message);
});
var requestConfig = RequestProxyConfig.Empty with { AllowResponseBuffering = enableBuffering };
var proxyError = await sut.ProxyAsync(httpContext, destinationPrefix, client, requestConfig, HttpTransformer.Default);
Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
Assert.Equal(enableBuffering != true, responseBody.BufferingDisabled);
}
[Fact]
public async Task ProxyAsync_RequestBodyCanceledAfterResponse_Reported()
{
@ -2142,6 +2175,8 @@ namespace Yarp.ReverseProxy.Proxy.Tests
public PipeWriter Writer => throw new NotImplementedException();
public bool BufferingDisabled { get; set; }
public int StatusCode { get; set; } = 200;
public string ReasonPhrase { get; set; }
public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
@ -2161,7 +2196,7 @@ namespace Yarp.ReverseProxy.Proxy.Tests
public void DisableBuffering()
{
throw new NotImplementedException();
BufferingDisabled = true;
}
public void OnCompleted(Func<object, Task> callback, object state)

Просмотреть файл

@ -23,7 +23,8 @@
"LoadBalancingPolicy": "Random",
"SessionAffinity": {
"Enabled": "true",
"Mode": "Cookie"
"Mode": "Cookie",
"AffinityKeyName": ".Yarp.Affinity"
},
"HealthCheck": {
"Active": {

Просмотреть файл

@ -64,7 +64,7 @@ namespace SampleClient.Scenarios
Console.WriteLine($"Response doesn't have Set-Cookie header.");
}
var affinityCookie = handler.CookieContainer.GetCookies(targetUri)[".Yarp.ReverseProxy.Affinity"];
var affinityCookie = handler.CookieContainer.GetCookies(targetUri)[".Yarp.Affinity"];
Console.WriteLine($"Affinity key stored on a cookie {affinityCookie.Value}");
}