dotnet / systemweb-adapters

MIT License
337 stars 59 forks source link

Forwarded Requests from ASP.NET Core to ASP.NET MVC are throwing an InvalidOperationException looking for IHttpResponseBufferingFeature #521

Closed michac closed 1 month ago

michac commented 2 months ago

Describe the bug

I have two projects: A legacy .NET Framework 4.8 MVC project, and a .NET 8 asp.net core project. The latter was generated using the Upgrade Assistant documented here: https://learn.microsoft.com/en-us/aspnet/core/migration/mvc?view=aspnetcore-8.0 Nothing is implemented in the Core application yet, so the expectation is that it will just behave as a proxy to the .NET Framework app. When a request comes in to the asp.net core project it is handled by the {**catch-all} handler, forwards the request, gets a 200 response from the .NET Framework project, then logs the InvalidOperationException. When I'm using HTTPS to access the asp.net core site the website does not work, but when I use HTTP instead the website does work despite the exception.

To Reproduce

I haven't made any custom changes to the generated code for the asp.net core application, except for the remoteapp setup described here: https://learn.microsoft.com/en-us/aspnet/core/migration/inc/remote-app-setup?view=aspnetcore-8.0 This is the entirety of my core application:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSystemWebAdapters();
builder.Services.AddHttpForwarder();

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSystemWebAdapters()
    .AddRemoteAppClient(options =>
    {
        options.RemoteAppUrl = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
        options.ApiKey = builder.Configuration["RemoteAppApiKey"];
    });

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

//app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthorization();
app.UseSystemWebAdapters();

app.MapDefaultControllerRoute();
app.MapForwarder("/{**catch-all}", app.Configuration["ProxyTo"]).Add(static builder => ((RouteEndpointBuilder)builder).Order = int.MaxValue);

app.Run();

Exceptions (if any)

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HN5B96O1HTFE", Request id "0HN5B96O1HTFE:00000029": An unhandled exception was thrown by the application.
      System.InvalidOperationException: Feature Microsoft.AspNetCore.SystemWebAdapters.Features.IHttpResponseBufferingFeature is not available
         at Microsoft.AspNetCore.SystemWebAdapters.FeatureCollectionExtensions.GetRequired[TFeature](IFeatureCollection features)
         at Microsoft.Extensions.DependencyInjection.RegisterAdapterFeaturesMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

Further technical details

Here's the request log when using HTTPS

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:7004/Account/Login?ReturnUrl=%2F - - -
trce: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[2]
      All hosts are allowed.
dbug: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[4]
      The request path /Account/Login does not match a supported file type
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1001]
      1 candidate(s) found for the request path '/Account/Login'
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1005]
      Endpoint '/{**catch-all}' with route pattern '/{**catch-all}' is valid for the request path '/Account/Login'
dbug: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[1]
      Request matched endpoint '/{**catch-all}'
trce: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[8]
      The endpoint does not specify the IRequestSizeLimitMetadata.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/{**catch-all}'
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
      Proxying to https://localhost:44301/Account/Login?ReturnUrl=%2F HTTP/2 RequestVersionOrLower no-streaming
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
      Received HTTP/2.0 response 200.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/{**catch-all}'
trce: Microsoft.AspNetCore.Server.Kestrel.Http2[49]
      Connection id "0HN5B96O1HTFE" sending HEADERS frame for stream ID 41 with length 48 and flags END_HEADERS.
trce: Microsoft.AspNetCore.Server.Kestrel.Http2[49]
      Connection id "0HN5B96O1HTFE" sending DATA frame for stream ID 41 with length 1182 and flags NONE.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HN5B96O1HTFE", Request id "0HN5B96O1HTFE:00000029": An unhandled exception was thrown by the application.
      System.InvalidOperationException: Feature Microsoft.AspNetCore.SystemWebAdapters.Features.IHttpResponseBufferingFeature is not available
         at Microsoft.AspNetCore.SystemWebAdapters.FeatureCollectionExtensions.GetRequired[TFeature](IFeatureCollection features)
         at Microsoft.Extensions.DependencyInjection.RegisterAdapterFeaturesMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
trce: Microsoft.AspNetCore.Server.Kestrel.Http2[37]
      Connection id "0HN5B96O1HTFE" received WINDOW_UPDATE frame for stream ID 0 with length 4 and flags 0x0.
dbug: Microsoft.AspNetCore.Server.Kestrel.Http2[35]
      Trace id "0HN5B96O1HTFE:00000029": HTTP/2 stream error "INTERNAL_ERROR". A Reset is being sent to the stream.
      Microsoft.AspNetCore.Connections.ConnectionAbortedException: An error occurred after the response headers were sent, a reset is being sent.
trce: Microsoft.AspNetCore.Server.Kestrel.Http2[49]
      Connection id "0HN5B96O1HTFE" sending RST_STREAM frame for stream ID 41 with length 4 and flags 0x0.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 GET https://localhost:7004/Account/Login?ReturnUrl=%2F - 200 1182 text/html;+charset=utf-8 39.6825ms

(the last line says request finished with 200 but in the browser the page is blank and if I go into developer tools it says the payload of the response is empty)

This is the log when using HTTP:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5004/Account/Login?ReturnUrl=%2F - - -
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[1]
      Connection id "0HN5B96O1HTHR" started.
trce: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[2]
      All hosts are allowed.
dbug: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[4]
      The request path /Account/Login does not match a supported file type
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1001]
      1 candidate(s) found for the request path '/Account/Login'
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1005]
      Endpoint '/{**catch-all}' with route pattern '/{**catch-all}' is valid for the request path '/Account/Login'
dbug: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[1]
      Request matched endpoint '/{**catch-all}'
trce: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[8]
      The endpoint does not specify the IRequestSizeLimitMetadata.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/{**catch-all}'
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
      Proxying to https://localhost:44301/Account/Login?ReturnUrl=%2F HTTP/2 RequestVersionOrLower no-streaming
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
      Received HTTP/2.0 response 200.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/{**catch-all}'
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HN5B96O1HTHQ", Request id "0HN5B96O1HTHQ:00000001": An unhandled exception was thrown by the application.
      System.InvalidOperationException: Feature Microsoft.AspNetCore.SystemWebAdapters.Features.IHttpResponseBufferingFeature is not available
         at Microsoft.AspNetCore.SystemWebAdapters.FeatureCollectionExtensions.GetRequired[TFeature](IFeatureCollection features)
         at Microsoft.Extensions.DependencyInjection.RegisterAdapterFeaturesMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET http://localhost:5004/Account/Login?ReturnUrl=%2F - 200 1182 text/html;+charset=utf-8 23.3622ms
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[10]
      Connection id "0HN5B96O1HTHQ" disconnecting.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[2]
      Connection id "0HN5B96O1HTHQ" stopped.
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[7]
      Connection id "0HN5B96O1HTHQ" sending FIN because: "The Socket transport's send loop completed gracefully."

Please include the following if applicable:

ASP.NET Framework Application:

ASP.NET Core Application:

twsouthwick commented 2 months ago

Thanks for the report! I haven't seen that happen myself, but it's a straight forward fix for this case :) once merged, you can try it out on the ci