Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Routing;
using TickerQ.Utilities.Entities;

namespace TickerQ.Dashboard.DependencyInjection
Expand Down Expand Up @@ -344,6 +345,15 @@ private static void MapPathBaseAware(this IApplicationBuilder app, string basePa
context.Request.PathBase = originalPathBase.Add(matchedPath);
context.Request.Path = remainingPath;

// Clear any endpoint matched by host-level routing so the branch's
// own UseRouting() re-evaluates against dashboard endpoints.
// Without this, host Map*() calls (e.g. MapStaticAssets().ShortCircuit())
// can cause the branch's routing middleware to skip evaluation — the
// EndpointRoutingMiddleware short-circuits when GetEndpoint() is non-null.
// This results in 405 responses for SignalR/WebSocket requests (#456).
context.SetEndpoint(null);
context.Request.RouteValues?.Clear();

try
{
await branch(context);
Expand Down
124 changes: 124 additions & 0 deletions tests/TickerQ.Tests/DashboardPathBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TickerQ.Dashboard.DependencyInjection;

Expand Down Expand Up @@ -272,4 +274,126 @@ private static async Task<IHost> CreateTestHost(
}

#endregion

#region MapPathBaseAware — endpoint routing coexistence (issue #456)

[Fact]
public async Task MapPathBaseAware_ClearsHostEndpoint_SoBranchRoutingCanReevaluate()
{
// Simulates the scenario in issue #456: host-level routing (e.g. MapStaticAssets)
// sets an endpoint before the dashboard branch runs. The branch must clear it so
// its own UseRouting() re-evaluates against dashboard endpoints.
var mapMethod = typeof(ServiceCollectionExtensions).GetMethod(
"MapPathBaseAware",
BindingFlags.NonPublic | BindingFlags.Static)!;

using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder.UseTestServer();
webBuilder.ConfigureServices(services =>
{
services.AddRouting();
});
webBuilder.Configure(app =>
{
// Simulate host-level routing setting an endpoint (like MapStaticAssets does)
app.Use(async (context, next) =>
{
var dummyEndpoint = new Endpoint(
_ => Task.CompletedTask,
new EndpointMetadataCollection(),
"DummyHostEndpoint");
context.SetEndpoint(dummyEndpoint);
context.Request.RouteValues["dummy"] = "value";
await next();
});

var configuration = new Action<IApplicationBuilder>(branch =>
{
branch.Run(async context =>
{
// Verify endpoint was cleared inside the branch
var endpoint = context.GetEndpoint();
var hasRouteValues = context.Request.RouteValues.Count > 0;
await context.Response.WriteAsync(
endpoint == null && !hasRouteValues ? "cleared" : "not-cleared");
});
});

mapMethod.Invoke(null, new object[] { app, "/dashboard", configuration });

app.Run(async context =>
{
await context.Response.WriteAsync("fallthrough");
});
});
})
.StartAsync();

var client = host.GetTestClient();
var response = await client.GetAsync("/dashboard/test");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("cleared", body);
}

[Fact]
public async Task MapPathBaseAware_WithHostEndpoint_NonMatchingPath_PreservesEndpoint()
{
// Non-matching paths should pass through without clearing the host endpoint
var mapMethod = typeof(ServiceCollectionExtensions).GetMethod(
"MapPathBaseAware",
BindingFlags.NonPublic | BindingFlags.Static)!;

using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder.UseTestServer();
webBuilder.ConfigureServices(services =>
{
services.AddRouting();
});
webBuilder.Configure(app =>
{
app.Use(async (context, next) =>
{
var dummyEndpoint = new Endpoint(
_ => Task.CompletedTask,
new EndpointMetadataCollection(),
"DummyHostEndpoint");
context.SetEndpoint(dummyEndpoint);
await next();
});

var configuration = new Action<IApplicationBuilder>(branch =>
{
branch.Run(async context =>
{
await context.Response.WriteAsync("branch-hit");
});
});

mapMethod.Invoke(null, new object[] { app, "/dashboard", configuration });

app.Run(async context =>
{
var endpoint = context.GetEndpoint();
await context.Response.WriteAsync(
endpoint?.DisplayName == "DummyHostEndpoint" ? "preserved" : "lost");
});
});
})
.StartAsync();

var client = host.GetTestClient();
var response = await client.GetAsync("/other/path");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("preserved", body);
}

#endregion
}
Loading