Serve static maintenance page with 503 when PocketBase is unreachable

This commit is contained in:
tleininger committed 2026-10-01 17:12:00 +02:00
1 parent ed6aa5d5ec
commit 7e56e83953
7 files changed
+500 -4

No files matched your search

+37
View File
@@ -2,6 +2,8 @@ using Elternbeirat.Contracts;
using Elternbeirat.PocketBase;
using Elternbeirat.Web.Components;
using Elternbeirat.Web.Features.Events;
using Elternbeirat.Web.Shared;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
@@ -25,6 +27,10 @@ builder.Services.AddHttpClient<PocketBaseClient>(client =>
client.Timeout = TimeSpan.FromSeconds(5);
});
// Shared by all requests, so the maintenance gate below asks PocketBase at most once
// every few seconds rather than on every page view.
builder.Services.AddSingleton<PocketBaseHealthCache>();
var app = builder.Build();
// NPM sits in front of the app and handles HTTPS. Without this line the app would
@@ -48,6 +54,37 @@ if (app.Environment.IsDevelopment() is false)
// is down (a 503) it would wrongly show "not found" instead of our own message.
app.UseAntiforgery();
// Maintenance gate: while PocketBase is down, every page gets the static
// maintenance.html with 503 instead of a half-empty page (the menu comes from
// PocketBase too). Routing has already picked the endpoint at this point, so the gate
// covers exactly the Razor component pages: /health, the calendar feeds, the static
// assets (incl. maintenance.html and its font) are not components and answer for
// themselves. A new endpoint is thus left alone unless it is a page. The health
// result is cached (PocketBaseHealthCache), so the page may come back a few seconds
// after PocketBase does. Retry-After tells well-behaved clients and crawlers when to
// come back; no-store keeps the 503 page out of any cache.
app.Use(async (context, next) =>
{
var isPage = context.GetEndpoint()?.Metadata.GetMetadata<ComponentTypeMetadata>() is not null;
var services = context.RequestServices;
if (!isPage
|| await services.GetRequiredService<PocketBaseHealthCache>().IsHealthyAsync(
services.GetRequiredService<PocketBaseClient>().IsHealthyAsync,
context.RequestAborted))
{
await next(context);
return;
}
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
context.Response.Headers.RetryAfter = "60";
context.Response.Headers.CacheControl = "no-store";
context.Response.ContentType = "text/html; charset=utf-8";
await context.Response.SendFileAsync(
app.Environment.WebRootFileProvider.GetFileInfo("maintenance.html"),
context.RequestAborted);
});
app.MapStaticAssets();
app.MapRazorComponents<App>();