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:
Родитель
c14613da75
Коммит
d2c7863f28
|
@ -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}");
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче