Fehlerseiten
Das Paket bringt gestaltete Seiten für die Statuscodes 400, 401, 403, 404 und 500 mit, dazu eine Seite für einen fehlgeschlagenen Anmeldevorgang am Identity Provider. Sie haben ein eigenes, schlankes Layout, es kommt ohne Bundles aus, damit auch dann noch etwas Lesbares erscheint, wenn die Anwendung selbst nicht mehr antwortet.
Die Einrichtung besteht aus zwei Aufrufen: AddSiwfErrorPages macht die
Einstellungen verfügbar, UseSiwfErrorPages hängt die Middleware ein, die
Statuscodes und unbehandelte Ausnahmen auf diese Seiten umleitet.
Die sechs Seiten
Sie kommen aus dem ErrorController des Pakets und sind erreichbar, sobald die Komponente referenziert ist – über /400, /401, /403, /404, /500 und /Error/RemoteAuthError.
@* Die Seiten laufen im eigenen Dokument statt direkt eingebettet: Sie bringen ihr eigenes Layout
(_LayoutError) samt Inline-Styles mit, das sonst die Doku-Seite überschreiben würde. Im Rahmen
bleibt beides isoliert, und gezeigt wird die echte Seite.
Die Routen kommen aus dem ErrorController des Pakets und sind erreichbar, sobald die Komponente
referenziert ist – ohne Zutun der Anwendung. *@
<div class="row g-3">
@foreach (var seite in new[]
{
("400", "Fehlerhafte Anfrage"),
("401", "Nicht angemeldet"),
("403", "Kein Zugriff"),
("404", "Nicht gefunden"),
("500", "Serverfehler"),
})
{
<div class="col-lg-6">
<p class="mb-1">
<strong>@seite.Item1</strong> – @seite.Item2
<a href="/@seite.Item1" target="_blank" rel="noopener" class="small ms-2">in neuem Tab öffnen</a>
</p>
<iframe src="/@seite.Item1"
title="Fehlerseite @seite.Item1"
class="w-100 border rounded"
style="height: 420px"></iframe>
</div>
}
<div class="col-lg-6">
<p class="mb-1">
<strong>RemoteAuthError</strong> – Anmeldung am Identity Provider fehlgeschlagen
<a href="/Error/RemoteAuthError" target="_blank" rel="noopener" class="small ms-2">in neuem Tab öffnen</a>
</p>
<iframe src="/Error/RemoteAuthError"
title="Fehlerseite RemoteAuthError"
class="w-100 border rounded"
style="height: 420px"></iframe>
</div>
</div>
Darstellung
400 – Fehlerhafte Anfrage in neuem Tab öffnen
401 – Nicht angemeldet in neuem Tab öffnen
403 – Kein Zugriff in neuem Tab öffnen
404 – Nicht gefunden in neuem Tab öffnen
500 – Serverfehler in neuem Tab öffnen
RemoteAuthError – Anmeldung am Identity Provider fehlgeschlagen in neuem Tab öffnen
Sechs fertige Seiten, alle mit demselben schlanken Layout (_LayoutError):
| Adresse | Seite | Wofür |
|---|---|---|
/400 |
Fehlerhafte Anfrage | Die Anfrage war nicht verwertbar. |
/401 |
Nicht angemeldet | Es fehlt eine gültige Anmeldung. |
/403 |
Kein Zugriff | Angemeldet, aber ohne Berechtigung. |
/404 |
Nicht gefunden | Die Adresse gibt es nicht. |
/500 |
Serverfehler | Nennt zusätzlich die Trace-Id des Vorgangs. |
/Error/RemoteAuthError |
Anmeldung fehlgeschlagen | Nach einem Fehler beim Identity Provider. |
Die Routen kommen aus dem ErrorController des Pakets und sind erreichbar, sobald die Komponente
referenziert ist, ohne eine Zeile in der Anwendung. Genau deshalb lassen sie sich hier im Rahmen
zeigen, obwohl diese Doku-Anwendung die Middleware gar nicht einhängt.
Das eigene Layout ist Absicht
_LayoutError bringt seine Gestaltung inline mit und lädt keine Bundles. Eine Fehlerseite muss auch
dann noch etwas Lesbares zeigen, wenn die Anwendung schon nicht mehr richtig arbeitet, ein Layout,
das erst Skripte und Stile nachlädt, wäre dafür der falsche Weg.
Die 500er-Seite nennt eine Trace-Id
Der ErrorController liest Activity.Current.TraceId, zeigt sie auf der Seite und schreibt
gleichzeitig einen Log.Error-Eintrag mit derselben Id. Wer anruft, kann sie durchgeben, der
Eintrag im Log ist damit ohne Suchen auffindbar. Das ist der praktische Grund, Anwender auf diese
Seite zu leiten statt auf eine allgemeine Fehlermeldung.
RemoteAuthError wird nicht automatisch angesteuert
Anders als die fünf Statuscode-Seiten hat sie keine Zahlen-Route. Sie ist als Ziel für die
Fehlerbehandlung der Authentifizierung gedacht (OnRemoteFailure) und braucht dort eine
ausdrückliche Umleitung. Ihr Text verweist auf LoginUrl aus den Einstellungen, damit jemand mit
einem veralteten Lesezeichen die richtige Adresse bekommt.
Einrichtung in der Anwendung
Zwei Hälften: AddSiwfErrorPages bindet die Section «ErrorPages» an das Setting, UseSiwfErrorPages hängt die Middleware ein. Die Tabs zeigen die Dateien dieser Anwendung im Original.
@* Kein eigenes Markup: Was hier zu zeigen ist, steht in Program.cs und appsettings.json – beide
als Code-Tabs daneben. Die Wirkung ist in den Rahmen des ersten Beispiels zu sehen: Logo und
Copyright-Zeile tragen die Werte aus der Section «ErrorPages».
Diese Anwendung ruft bewusst nur AddSiwfErrorPages auf, nicht UseSiwfErrorPages – warum, steht
im Anleitungs-Tab. *@
<p class="mb-2">Der Vergleich lässt sich direkt anstellen:</p>
<ul class="mb-0">
<li>
<a href="/404" target="_blank" rel="noopener">Fehlerseite 404 dieser Anwendung</a> –
mit dem Logo aus <code>appsettings.json</code> (Breite 160 px) und der Copyright-Zeile
«SIWF» beziehungsweise «ISFM» je nach Sprache.
</li>
<li>
Ohne die Section <code>ErrorPages</code> stünde dort das kleine SIWF-Standardlogo und
durchgehend «SIWF» – der Rückfall, den die Views mitbringen.
</li>
</ul>
using System;
using System.Linq;
using databinding.WebSecurity.Headers;
using databinding.WebSecurity.HtmlSanitizer.Settings;
using FMH.ICT.Basis.Web.Security.Security;
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.BenutzerPraesenzCheck;
using FMH.Komponente.Infrastruktur;
using FMH.Komponente.Infrastruktur.Bundle;
using FMH.Komponente.Infrastruktur.Mvc.Errors;
using FMH.Komponente.Serilog.Ingestion;
using FMH.Komponente.Tabelle.Erweiterungen;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using Serilog;
using Serilog.Sinks.SystemConsole.Themes;
namespace FMH.Komponente.WebUI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
builder.Host.UseSerilog((context, services, config) => config.Enrich.WithRequestHeader("User-Agent")
.Enrich.WithClientIp()
////.Enrich.With(services.GetService<BenutzerTraceIdNet6Enricher>()) // Einkommentieren mit isAuthEnabled = true
.ReadFrom.Configuration(configuration)
.WriteTo.Console(theme: ConsoleTheme.None));
// Stellt sicher, dass Static Web Assets referenzierter Razor Class Libraries (z.B. FMH.Komponente.Basis)
// unabhängig von der Hosting-Umgebung auflösbar sind, damit WebOptimizer deren SCSS-/JS-Dateien findet.
builder.WebHost.UseStaticWebAssets();
// Add services to the container.
// AdditionalMetadataProvider registrieren, damit [AdditionalMetadata(...)] die
// ModelMetadata.AdditionalValues füllt, Voraussetzung für die Listen-EditorTemplates
// (DropdownList/RadioList/CheckboxList/…, Schlüssel "Selection") sowie für die
// Display-Optionen Bold/ValueColorClass und die Icons in Auswahlbox/CheckInfo.
builder.Services.AddControllersWithViews(options =>
{
options.AddFmhWidgetOptions();
// Nötig für die Tabellen-Beispiele mit AjaxDataUrl: Ohne den Binder darin kommt
// GridDatatableParameter leer im Endpunkt an (DataTables sendet start/length/
// order[0][dir]/search[value], nicht Skip/Take/…). AddFmhWidgetOptions kann ihn
// nicht mitbringen, FMH.Komponente.Basis referenziert die Tabelle nicht.
options.AddFmhTabelleOptions();
});
builder.Services.AddBundledAssets();
// Links im Richtext zulassen. Die Voreinstellung des Sanitizers kennt weder das a-Tag
// noch Attribute, ein eingefügter Link würde samt Text entfernt.
//
// Muss VOR der ersten Verwendung stehen: Serverseitig liest HtmlWhitelistSanitizer die
// Werte über HtmlSanitizerRichtextDefauls.Options(), clientseitig meldet sie der
// Endpunkt /api/widgets/HtmlSanitizer/settings an DOMPurify. Beide Seiten arbeiten damit
// gegen dieselbe Liste.
HtmlSanitizerRichtextDefauls.AddAllowedTags("a");
HtmlSanitizerRichtextDefauls.AddAllowedAttributes("href", "target", "rel");
// Die Upload-Validierung (DocumentUploadRequired) liest die Anzahl bereits
// vorhandener Dokumente aus dem Request – dafür braucht sie den HttpContext.
builder.Services.AddHttpContextAccessor();
// Bindet die Section "BenutzerPraesenzPruefer" der appsettings.json an das Setting und
// macht es über IOptions injizierbar, die Demo-Seite nutzt es genau so wie ein Projekt.
builder.Services.AddBenutzerPraesenzPruefer(builder.Configuration);
// Bindet die Section "ErrorPages" an das Setting. Die Fehlerseiten des Pakets
// Infrastruktur sind über /400, /401, /403, /404 und /500 ohnehin erreichbar; erst
// diese Zeile gibt ihnen Logo und Texte dieser Anwendung.
//
// Bewusst OHNE app.UseSiwfErrorPages(...): Die Middleware würde in Betrieb die
// Umleitung auf die Fehlerseiten übernehmen und damit UseExceptionHandler ersetzen.
// Beim Entwickeln hält sie sich über ihren Entwicklermodus selbst zurück, sie bleibt
// hier trotzdem aussen vor, damit die Pipeline dieser Anwendung dem entspricht, was
// die Code-Tabs der Demo-Seite «Fehlerseiten» zeigen.
builder.Services.AddSiwfErrorPages(builder.Configuration);
var app = builder.Build();
app.UseSiwfLogIngestion();
var isDev = app.Environment.IsDevelopment();
// Configure the HTTP request pipeline.
if (!isDev)
{
app.UseExceptionHandler("/Home/Error");
// Ausnahme für die Integrationstests: Die Seiten der Area Integration zeigen auch
// ausserhalb von Development die Entwickler-Fehlerseite. Der Test der doppelten
// Render-Aufrufe liest dort die Meldung der Ausnahme, die allgemeine Fehlerseite
// verschluckt sie. Liegt innerhalb von UseExceptionHandler und fängt deshalb zuerst.
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/Integration"),
integration => integration.UseDeveloperExceptionPage());
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
var headerCollection = new HeaderCollectionBuilder().AddConnectSrcLocalhost()
.AddUnsafeInlineScriptSrc()
.AddBlazor()
.Build();
// frame-src/frame-ancestors auf 'self': Die Doku-Seite AnalyseDemo/Demo/AnalyseSeite zeigt
// die Analyse-Seite derselben Anwendung in einem Rahmen. frame-src erlaubt das Einbetten,
// frame-ancestors das Eingebettetwerden, beides bleibt auf die eigene Herkunft beschränkt.
// Der HeaderCollectionBuilder kennt dafür keine Kurzform, deshalb hier am fertigen
// HeaderCollection ergänzt, statt die Richtlinie neu aufzubauen.
headerCollection.GetContentSecurityPolicyBuilder()
.AddFrameSrc(direktive => direktive.Self())
.AddFrameAncestors(direktive => direktive.Self());
app.UseHeaderSecurity(headerCollection);
app.UseHttpsRedirection();
app.UseWebOptimizer();
// Native ES-Module werden nicht gebündelt. Der Versionsstempel wirkt nur auf die jeweilige
// Einstiegsdatei, nicht auf die transitiv importierten Module. Deshalb erzwingen wir für die
// Modulverzeichnisse eine ETag-Revalidierung bei jedem Laden, damit Änderungen an jeder Datei
// im Modul-Graphen sofort sichtbar werden. Die Verzeichnisse liefert der Modulkatalog, damit
// ein neues Modul hier nicht nachgetragen werden muss.
var modulVerzeichnisse = app.Services.GetRequiredService<ModuleScriptRegistry>()
.GetModuleDirectories();
// Nur für die Demo: Dateitypen ohne bekannten MIME-Typ liefert UseStaticFiles nicht aus.
// Die Beispiele des DocumentViewers brauchen aber ein Format, das er bewusst nicht
// darstellen kann, dort ist eine Outlook-Nachricht der realistische Fall.
var inhaltstypen = new FileExtensionContentTypeProvider();
inhaltstypen.Mappings[".msg"] = "application/vnd.ms-outlook";
// Nicht nur für die Demo: pdf.js im PDFViewer lädt Zeichensatztabellen (.bcmap) und
// Farbprofile (.icc) nach, beide kennt UseStaticFiles nicht und antwortet mit 404.
inhaltstypen.Mappings[".bcmap"] = "application/octet-stream";
inhaltstypen.Mappings[".icc"] = "application/vnd.iccprofile";
app.UseStaticFiles(
new StaticFileOptions
{
ContentTypeProvider = inhaltstypen,
OnPrepareResponse = context =>
{
var pfad = context.Context.Request.Path.Value;
if (pfad != null && modulVerzeichnisse.Any(verzeichnis =>
pfad.StartsWith(verzeichnis, StringComparison.OrdinalIgnoreCase)))
{
context.Context.Response.Headers[HeaderNames.CacheControl] = "no-cache, must-revalidate";
}
},
});
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute("areas", "{area:exists}/{controller=Home}/{action=Index}/{id?}");
// Sprechende Adressen für die Anleitungen: /Anleitungen/logging/ueberblick. Beide Segmente
// sind Pflicht, deshalb fällt /Anleitungen weiter auf die Default-Route und landet auf Index.
app.MapControllerRoute("anleitungen", "Anleitungen/{gruppe}/{seite}", new { controller = "Anleitungen", action = "Seite" });
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"BenutzerPraesenzPruefer": {
"IntervalInSekunden": 2,
"Wiederholungen": 5,
"LogoutUrl": "/BenutzerPraesenzCheck/PraesenzPruefung/Abmeldung",
"PruefEndpunkt": "/BenutzerPraesenzCheck/PraesenzPruefung/Status",
"IstAktiviert": true,
"AuthCheckIntervalInSekunden": 45,
"IstAuthCheckAktiviert": false
},
"ErrorPages": {
"LogoUrl": "/_content/FMH.Komponente.Infrastruktur/images/logo_siwf.png",
"LogoAlternativText": {
"de": "SIWF",
"fr": "ISFM",
"en": "SIWF",
"it": "SIWF"
},
"LogoUrlWidthInPixel": 160,
"LoginUrl": "https://login.siwf.ch"
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Error"
}
},
"Properties": {
"Release": "0.0.0.0-development",
"App": "FMH.Komponente.WebUI"
},
"WriteTo": [
{
"Name": "Seq",
"Args": {
"ServerUrl": "http://127.0.0.1:5341",
"ApiKey": ""
}
}
]
}
}
Darstellung
Der Vergleich lässt sich direkt anstellen:
-
Fehlerseite 404 dieser Anwendung –
mit dem Logo aus
appsettings.json(Breite 160 px) und der Copyright-Zeile «SIWF» beziehungsweise «ISFM» je nach Sprache. -
Ohne die Section
ErrorPagesstünde dort das kleine SIWF-Standardlogo und durchgehend «SIWF» – der Rückfall, den die Views mitbringen.
Die Einrichtung besteht aus zwei Aufrufen. Sie zu trennen ist die häufigste Stolperstelle: Fehlt der erste, erscheinen die Seiten trotzdem, aber immer mit dem SIWF-Standardlogo.
// 1. Einstellungen verfügbar machen
builder.Services.AddSiwfErrorPages(builder.Configuration);
// 2. Middleware einhängen
app.UseSiwfErrorPages(app.Environment.IsDevelopment());
AddSiwfErrorPages bindet die Section ErrorPages an ErrorPagesSetting und protokolliert die
Werte einmalig beim Start. Alle sechs Views beziehen das Setting über IOptions<ErrorPagesSetting>
ohne die Registrierung greift überall der Rückfall.
Die Section in der appsettings.json
"ErrorPages": {
"LogoUrl": "/_content/FMH.Komponente.Infrastruktur/images/logo_siwf.png",
"LogoAlternativText": {
"de": "SIWF",
"fr": "ISFM",
"en": "SIWF",
"it": "SIWF"
},
"LogoUrlWidthInPixel": 160,
"LoginUrl": "https://login.siwf.ch"
}
| Eigenschaft | Wirkung | Ohne Angabe |
|---|---|---|
LogoUrl |
Adresse des Logos auf der Seite. | /_content/FMH.Komponente.Infrastruktur/images/logo_siwf_small.png |
LogoUrlWidthInPixel |
Breite in Pixel, zum Abstimmen auf das eigene Logo. | ohne Breitenangabe gerendert |
LogoAlternativText |
Alternativtext je Sprache, Schlüssel als zweistelliger Iso-Code. | SIWF |
LoginUrl |
Adresse des Identity Providers, eingesetzt im Text von RemoteAuthError. |
leer |
Das Paket liefert vier Logos mit: logo_siwf.png (306×128), logo_siwf_small.png (181×68),
logo_siwf_xs.png (130×54) und logo_fmh.png (268×128).
Der Alternativtext erscheint zweimal
Er steht nicht nur am Bild, sondern auch im Seitentitel (404 – Nicht gefunden - SIWF) und in der
Copyright-Zeile (© 2026 - SIWF). Deshalb gehört dort nur das Kürzel hinein, nicht „Logo des
SIWF". Fehlt die aktuelle Sprache im Wörterbuch, greift der erste Eintrag; fehlt das Wörterbuch
ganz, greift SIWF.
Was die Middleware tut
UseSiwfErrorPages hängt StatusCodeExceptionHandler ein. Der:
- behandelt die Statuscodes 400, 401, 403, 404 und 500, aber nur, wenn die Antwort noch nicht
begonnen hat (
Response.HasStarted), sonst entstünden Folgefehler bei laufenden Umleitungen; - fängt unbehandelte Ausnahmen und schreibt
Log.Fatalmit Methode und Adresse; - entscheidet die Form der Antwort allein an der Anfrage: Ein Seitenaufruf wird auf
/<code>umgeleitet, ein Aufruf mit JSON bekommt{ "State": <code>, "message": … }und behält seinen Statuscode. Einstellbar ist das nicht, und es gilt für Statuscodes und Ausnahmen gleich.
| Seitenaufruf | Aufruf mit JSON | |
|---|---|---|
| Statuscode 404 | 302 auf /404 |
404 mit { "State": 404, "message": "Status Code 404" } |
| Unbehandelte Ausnahme | 302 auf /500 |
500 mit { "State": 500, "message": "<Meldung>" } |
Der Entwicklermodus hält die Middleware zurück
UseSiwfErrorPages(app.Environment.IsDevelopment()) gibt die Angabe weiter. Ist sie gesetzt, dann:
- bleibt jeder Statuscode stehen, es gibt weder Umleitung noch JSON-Rumpf;
- wird eine unbehandelte Ausnahme weitergegeben statt beantwortet.
Das Weitergeben ist der Sinn der Sache: Nur eine Ausnahme, welche die Middleware verlässt, erreicht die Entwickler-Fehlerseite von ASP.NET mit Meldung, Stapel und Anfrage. Würde sie hier beantwortet, blieben beim Entwickeln allein die Angaben im Protokoll.
Diese Doku-Anwendung ruft bewusst nur die erste Hälfte auf
In Program.cs steht AddSiwfErrorPages, aber kein UseSiwfErrorPages. Die Seiten sind
trotzdem über /400 bis /500 erreichbar, und dank AddSiwfErrorPages tragen sie Logo und Texte
dieser Anwendung. Beide Dateien stehen als Code-Tabs daneben.
Mit UseSiwfErrorPages(app.Environment.IsDevelopment()) liesse sich die Middleware inzwischen
einhängen, ohne beim Entwickeln die Entwickler-Fehlerseite zu verlieren; der Entwicklermodus hält
sie zurück. Die Doku-Anwendung lässt sie dennoch aussen vor, damit ihre Pipeline dem entspricht, was
die Code-Tabs zeigen, und ein Fehler hier immer auf demselben Weg sichtbar wird.
Was zu beachten ist
- Nicht mit
UseStatusCodePageskombinieren. Beide behandeln dieselben Statuscodes; welche Antwort gewinnt, hängt an der Reihenfolge in der Pipeline und ist keine Eigenschaft, auf die man sich stützen sollte. - Die JSON-Erkennung liest den Inhaltstyp der Anfrage, über
HttpRequest.HasJsonContentType()des Frameworks. Parameter und Schreibweise sind ohne Bedeutung,application/json; charset=utf-8undAPPLICATION/JSONgelten also, und die Ausprägungen mit Endung+jsonwieapplication/problem+jsonebenso. Nicht dazu gehörttext/json. - Ein GET gilt nie als JSON-Anfrage. Ohne Rumpf trägt eine Anfrage keinen Inhaltstyp, und der
Accept-Header wird nicht gelesen. Ein Aufruf aus JavaScript, der Daten nur abholt, bekommt deshalb die Umleitung auf die HTML-Seite. Wer die Statuscodes dort auswerten will, prüft die Antwort auf302. - Im Entwicklermodus wird die Ausnahme weitergegeben. Wer die Middleware dort einhängt, braucht
hinter ihr etwas, das die Ausnahme aufnimmt. In der Entwicklung ist das die
Entwickler-Fehlerseite, die
WebApplicationvon selbst einhängt; wer sie abgeschaltet hat, bekommt eine leere 500er-Antwort. - Ohne
AddSiwfErrorPagessind Logo und Texte der Rückfall, und das fällt erst auf, wenn tatsächlich eine Fehlerseite erscheint. Ein Aufruf von/404nach dem Einrichten ist deshalb die einfachste Kontrolle. - Der Protokolleintrag beim Start ist derzeit nicht zu sehen.
AddSiwfErrorPagesruftLogSetting()auf, das über den statischen Serilog-Logger schreibt. Die Dienste werden aber vorbuilder.Build()registriert, zu diesem Zeitpunkt istLog.Loggernoch der stille Standard. Das gilt genauso fürAddBenutzerPraesenzPruefer; wer die Werte im Log sehen will, protokolliert sie nach dem Bauen der Anwendung erneut.