Smoke-test /health for the reachable (200) and unreachable (503) cases

This commit is contained in:
tleininger committed 2026-09-30 21:07:40 +02:00
1 parent c67c71ea28
commit 666614a3b2
4 files changed
+110 -28

No files matched your search

+31 -21
View File
@@ -6,52 +6,50 @@ using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents();
// The typed client reads content from PocketBase over its REST API. The base URL
// comes from configuration: appsettings for local dev, the PocketBase__BaseUrl
// env var on the server (see compose.yaml).
// comes from config: appsettings for local dev, the PocketBase__BaseUrl env var
// on the server (see compose.yaml).
var pocketBaseUrl = builder.Configuration["PocketBase:BaseUrl"]
?? throw new InvalidOperationException("PocketBase:BaseUrl is not configured.");
builder.Services.AddHttpClient<PocketBaseClient>(client =>
client.BaseAddress = new Uri(pocketBaseUrl));
{
client.BaseAddress = new Uri(pocketBaseUrl);
// Every page waits for PocketBase. The default wait is 100 seconds, so if
// PocketBase is down the page would hang that long. 5 seconds fails fast instead.
client.Timeout = TimeSpan.FromSeconds(5);
});
var app = builder.Build();
// NPM terminates TLS and is the only way to reach the container (no port
// mapping in production). Without UseForwardedHeaders the app sees every request
// as HTTP and with the proxy IP instead of the client IP.
// KnownNetworks/KnownProxies are deliberately empty because only NPM reaches
// the container.
// NPM sits in front of the app and handles HTTPS. Without this line the app would
// think every request is plain HTTP and would see NPM's IP, not the visitor's.
// KnownNetworks/KnownProxies are left empty on purpose: only NPM reaches the app.
app.UseForwardedHeaders(
new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() is false)
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// No UseHsts() and no UseHttpsRedirection(): NPM sets HSTS and terminates
// TLS. Both here would create a redirect loop behind the proxy.
// No UseHsts() and no UseHttpsRedirection() here: NPM already handles HTTPS.
// Adding them behind NPM would send the browser into a redirect loop.
}
// No UseStatusCodePagesWithReExecute: the Router renders the NotFoundPage
// (Pages.NotFound) in process for both an unmatched route and an explicit
// NavigationManager.NotFound() from a component (see Routes.razor). Re-executing
// every error code to /not-found would also turn a 503 (content source down) into
// a "not found" page; leaving it out lets those components keep their own
// "temporarily unavailable" markup and 503 status.
// Don't add UseStatusCodePagesWithReExecute here. Blazor already shows its own
// not-found page. That middleware would catch *every* error code, so when PocketBase
// is down (a 503) it would wrongly show "not found" instead of our own message.
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>();
// Subscribable calendar feed of all events. A minimal API endpoint rather than
// a Razor page because it returns text/calendar, not HTML. Reads from PocketBase
// per request; an unreachable source yields an empty calendar, not a 500.
// Calendar feed people can subscribe to. It returns calendar data, not a web page,
// so it is a plain endpoint, not a Razor page. If PocketBase is down it returns an
// empty calendar rather than an error.
app.MapGet("/events.ics", async (PocketBaseClient pocketBase, CancellationToken token) =>
{
IReadOnlyList<Event> events;
@@ -67,4 +65,16 @@ app.MapGet("/events.ics", async (PocketBaseClient pocketBase, CancellationToken
return Results.Text(IcsCalendar.Build(events), "text/calendar; charset=utf-8");
});
// Status page for an outside monitor (Uptime Kuma) to check. One check tells all
// three states apart: no reply = the app is down, 503 = the app runs but PocketBase
// is down, 200 = both are fine. It talks to PocketBase directly, not through the
// page-rendering code, so it can still report when that code is the thing failing.
app.MapGet("/health", async (PocketBaseClient pocketBase, CancellationToken token) =>
await pocketBase.IsHealthyAsync(token)
? Results.Text("healthy", "text/plain; charset=utf-8")
: Results.Text(
"PocketBase unreachable",
"text/plain; charset=utf-8",
statusCode: StatusCodes.Status503ServiceUnavailable));
app.Run();