-
-
Notifications
You must be signed in to change notification settings - Fork 84
Description
When proxying through a controller, there is a hang when handling a POST or PUT request with either a Content-Type: application/x-www-form-urlencoded
or Content-Type: multipart/form-data
header.
I've reproduced it starting with a simple Web API project, with the following changs:
- Adding
services.AddProxies()
toConfigureServices()
. - Removing
app.UseHttpsRedirection()
fromConfigure()
so we can avoid certificate issues. - Adding
app.RunProxy(proxy => proxy.UseHttp("http://localhost:5000/proxied"))
to the end ofConfigure()
(afterUseEndpoints()
), so that we can try this proxy method, but only if the request doesn't match a valid route. Deliberately adds to the path, so that the initial request doesn't have to match a valid route when the proxied request does. - Creating the following controller, which handles that variant of proxying, and also has the destination we're proxying to:
[ApiController]
public class ProxyController : ControllerBase {
[Route("proxy/{**rest}")]
public Task ProxyAsync(string rest) {
return this.HttpProxyAsync($"http://localhost:5000/{rest}");
}
[Route("echo")]
[Route("proxied/echo2")]
public async Task<ActionResult<string>> EchoAsync() {
using var sr = new StreamReader(Request.Body);
return await sr.ReadToEndAsync();
}
}
I've used an API testing client (in my case the Firefox extension RESTer) to send various requests to this at https://localhost:5001/proxy/echo
(proxies to localhost:5000/echo
via the controller). As long as the request does not have one of the mentioned Content-Type headers, or has not content, this works fine. But with either of these content types, it returns an exception:
Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Reading the request body timed out due to data arriving too slowly. See MinRequestBodyDataRate.
at Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException.Throw(RequestRejectionReason reason)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1ContentLengthMessageBody.ReadAsyncInternal(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
at System.IO.StreamReader.ReadBufferAsync(CancellationToken cancellationToken)
at System.IO.StreamReader.ReadToEndAsyncInternal()
at ProxyTest.Controllers.ProxyController.EchoAsync() in /home/<removed>/Projects/ProxyTest/Controllers/ProxyController.cs:line 22
at lambda_method(Closure , Object )
at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
HEADERS
=======
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/x-www-form-urlencoded
Host: localhost:5000
TE: trailers
Request-Id: |b5df5be7-411c1e27a666778b.1.
Content-Length: 14
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: https
X-Forwarded-Host: localhost:5001
Forwarded: proto=https;host=localhost:5001;by=127.0.0.1;for=127.0.0.1;
But everything works correctly when requesting https://localhost:5001/echo2
(doesn't match a route, so proxies to localhost:5000/proxied/echo2
).
This must be the controller mucking around with something when handling form data, but I don't know what...