Skip to content

Hang when using a controller to proxy form #57

@PreferLinux

Description

@PreferLinux

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() to ConfigureServices().
  • Removing app.UseHttpsRedirection() from Configure() so we can avoid certificate issues.
  • Adding app.RunProxy(proxy => proxy.UseHttp("http://localhost:5000/proxied")) to the end of Configure() (after UseEndpoints()), 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...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions