Skip to content

Commit a075a7f

Browse files
committed
Add dev-only YARP proxy from back-office Kestrel listener to rsbuild dev server
1 parent 8245f62 commit a075a7f

4 files changed

Lines changed: 94 additions & 1 deletion

File tree

application/AppHost/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
// Second Kestrel port for back-office.dev.localhost so localhost mirrors the Azure post-split
9191
// topology where back-office has its own external ingress and AppGateway is not in the path.
9292
.WithEnvironment("BACK_OFFICE_KESTREL_PORT", ports.BackOfficeApi.ToString())
93+
// BackOfficeDevStaticProxy forwards /static/* and HMR traffic on the back-office Kestrel listener
94+
// to the rsbuild dev server. Dev-only; production builds serve a baked bundle from disk.
95+
.WithEnvironment("BACK_OFFICE_STATIC_PORT", ports.BackOfficeStatic.ToString())
9396
// Back-office bundle URLs target the dedicated Kestrel port directly (no AppGateway).
9497
.WithEnvironment("BACK_OFFICE_PUBLIC_URL", backOfficeBaseUrl)
9598
.WithEnvironment("BACK_OFFICE_CDN_URL", backOfficeBaseUrl)

application/account/Api/Account.Api.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<IncludeAssets>runtime; build; native; contentFiles; analyzers; buildTransitive</IncludeAssets>
3939
<PrivateAssets>all</PrivateAssets>
4040
</PackageReference>
41+
<PackageReference Include="Yarp.ReverseProxy"/>
4142
</ItemGroup>
4243

4344
<!--
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Net;
2+
using System.Net.Security;
3+
using Microsoft.AspNetCore.Http.Extensions;
4+
using Yarp.ReverseProxy.Forwarder;
5+
6+
namespace Account.Api;
7+
8+
// Dev-only proxy that forwards back-office static-asset and HMR traffic from the back-office Kestrel
9+
// listener (BACK_OFFICE_KESTREL_PORT) to the rsbuild dev server (BACK_OFFICE_STATIC_PORT). Production
10+
// builds bake the bundle into BackOfficeWebApp/dist and serve it via UseStaticFiles, so this proxy
11+
// is registered only in development. The proxy must run before authentication so anonymous browsers
12+
// can fetch /static/*, /rsbuild-hmr, and HMR /{filename}.hot-update.{ext} during the SPA bootstrap.
13+
public static class BackOfficeDevStaticProxy
14+
{
15+
public static IServiceCollection AddBackOfficeDevStaticProxy(this IServiceCollection services)
16+
{
17+
services.AddHttpForwarder();
18+
return services;
19+
}
20+
21+
public static IApplicationBuilder UseBackOfficeDevStaticProxy(this WebApplication app, string backOfficeHostname)
22+
{
23+
if (!app.Environment.IsDevelopment()) return app;
24+
25+
var rawPort = Environment.GetEnvironmentVariable("BACK_OFFICE_STATIC_PORT");
26+
if (!int.TryParse(rawPort, out var staticPort) || staticPort <= 0) return app;
27+
28+
var destinationPrefix = $"https://localhost:{staticPort}";
29+
var forwarder = app.Services.GetRequiredService<IHttpForwarder>();
30+
var invoker = new HttpMessageInvoker(new SocketsHttpHandler
31+
{
32+
UseProxy = false,
33+
AllowAutoRedirect = false,
34+
AutomaticDecompression = DecompressionMethods.None,
35+
UseCookies = false,
36+
// The rsbuild dev server uses a self-signed certificate from Aspire; trust it locally.
37+
SslOptions = new SslClientAuthenticationOptions
38+
{
39+
RemoteCertificateValidationCallback = (_, _, _, _) => true
40+
}
41+
}
42+
);
43+
44+
app.UseWhen(
45+
context => IsBackOfficeStaticRequest(context, backOfficeHostname),
46+
branch => branch.Run(async context =>
47+
{
48+
var error = await forwarder.SendAsync(context, destinationPrefix, invoker, ForwarderRequestConfig.Empty);
49+
if (error != ForwarderError.None && !context.Response.HasStarted)
50+
{
51+
var feature = context.Features.Get<IForwarderErrorFeature>();
52+
app.Logger.LogWarning(
53+
feature?.Exception,
54+
"Back-office dev static proxy failed for {Url}: {Error}",
55+
context.Request.GetDisplayUrl(),
56+
error
57+
);
58+
context.Response.StatusCode = StatusCodes.Status502BadGateway;
59+
}
60+
}
61+
)
62+
);
63+
return app;
64+
}
65+
66+
private static bool IsBackOfficeStaticRequest(HttpContext context, string backOfficeHostname)
67+
{
68+
if (!context.Request.Host.Host.Equals(backOfficeHostname, StringComparison.OrdinalIgnoreCase)) return false;
69+
70+
var path = context.Request.Path.Value;
71+
if (string.IsNullOrEmpty(path)) return false;
72+
73+
// /static/* covers all bundled JS/CSS chunks. /rsbuild-hmr is rsbuild's HMR WebSocket endpoint
74+
// (HTTP upgrade is handled by IHttpForwarder). The .hot-update.{ext} pattern catches HMR-delta
75+
// chunks rsbuild emits at the SPA root rather than under /static.
76+
if (path.StartsWith("/static/", StringComparison.OrdinalIgnoreCase)) return true;
77+
if (path.StartsWith("/rsbuild-hmr", StringComparison.OrdinalIgnoreCase)) return true;
78+
if (path.Contains(".hot-update.", StringComparison.OrdinalIgnoreCase)) return true;
79+
80+
return false;
81+
}
82+
}

application/account/Api/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Security.Claims;
22
using Account;
3+
using Account.Api;
34
using Microsoft.Extensions.Options;
45
using SharedKernel.Authentication;
56
using SharedKernel.Authentication.BackOfficeIdentity;
@@ -19,7 +20,8 @@
1920
// Configure dependency injection services like Repositories, MediatR, Pipelines, FluentValidation validators, etc.
2021
builder.Services
2122
.AddApiServices([Assembly.GetExecutingAssembly(), Configuration.Assembly], ApiDocumentLayout.AccountAndBackOffice)
22-
.AddAccountServices();
23+
.AddAccountServices()
24+
.AddBackOfficeDevStaticProxy();
2325

2426
var app = builder.Build();
2527

@@ -52,6 +54,11 @@
5254
app.MapGet("/login", Results.Unauthorized).RequireHost(backOfficeHostname);
5355
}
5456

57+
// Dev-only: forward back-office static-asset and HMR traffic on the back-office Kestrel listener to
58+
// the rsbuild dev server. Registered before UseApiServices so the conditional branch short-circuits
59+
// matching requests before the auth-gated SPA fallback.
60+
app.UseBackOfficeDevStaticProxy(backOfficeHostname);
61+
5562
app
5663
.UseApiServices() // Add common configuration for all APIs like Swagger, HSTS, and DeveloperExceptionPage.
5764
.UseHostScopedSinglePageAppFallback(

0 commit comments

Comments
 (0)