Files
irrlichtandClaude Opus 5.5 df0cfec132 Code-Review: tote Stellen entfernt, Umwege vereinfacht
- SQLite-Import, migrate.sh, notes/migrations.md und altes jQuery-Skript entfernt
- Backend: scanEntry/queryEntries statt doppelter Scan-Liste, Feed-Queries
  direkt statt feedWhere, Thread ohne Map-Umweg, gemeinsame Helfer für
  Cookie-Löschen und Mediennamen, usernameTaken
- Frontend: PostForm für Beitrag und Antwort, EntryCard in Deleted/LiveCard
  aufgeteilt, tote CSS-Regeln und Typen entfernt, Strich-Regeln zusammengelegt
- Rechtstexte verlinken /stats; Kommentare ohne Vorgeschichte

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
2026-09-27 15:03:22 +02:00

168 lines
6.0 KiB
Go

package main
import (
"errors"
"log"
"net/http"
)
// Fehler-Taxonomie: trennt, wie schwer ein Fehler ist (Kind), was genau
// passiert ist (Code) und was der Nutzer lesen soll (Msg).
//
// Der Sinn der Trennung: das Frontend zeigt Msg an und markiert Field, ein
// Client braucht Code, um ohne Parsen deutscher Strings reagieren zu können,
// und der HTTP-Status ergibt sich allein aus Kind.
type Kind uint8
const (
KindInternal Kind = iota // Bug oder Infrastruktur -- Ursache nie nach außen
KindInvalid // Eingabe passt nicht
KindUnauth // nicht angemeldet
KindForbidden // angemeldet, aber nicht erlaubt
KindNotFound // Objekt existiert nicht (oder soll nicht sichtbar sein)
KindConflict // Name vergeben, Doppel-Post
KindTooLarge // Upload über dem Limit
KindRateLimited // zu viele Anfragen
KindMethodNotAllowed // Route existiert, aber nicht mit dieser Methode
)
// Error ist der Fehlertyp, den die Service-Schicht nach oben gibt. Kind ist
// bewusst kein HTTP-Status: die Zuordnung passiert erst im Adapter, damit ein
// künftiger CLI- oder App-Client dieselben Fehler auf eigene Weise abbilden
// kann (Exit-Code, Dialog, Retry).
type Error struct {
Kind Kind
Code string // stabil und maschinenlesbar, Schema "<bereich>.<sache>"
Msg string // deutsche Nutzermeldung
Field string // optional: betroffenes Formularfeld
Fields map[string]string // optional: mehrere Feldfehler auf einmal
cause error // nur fürs Log, nie für den Nutzer
}
func (e *Error) Error() string { return e.Code + ": " + e.Msg }
// Unwrap gibt die eingepackte Ursache frei, damit errors.Is/As über den
// Taxonomie-Fehler hinweg bis zum ursprünglichen Fehler (etwa sql.ErrNoRows)
// durchsuchen kann.
func (e *Error) Unwrap() error { return e.cause }
func Invalid(code, msg string) *Error { return &Error{Kind: KindInvalid, Code: code, Msg: msg} }
func Unauth(code, msg string) *Error { return &Error{Kind: KindUnauth, Code: code, Msg: msg} }
func Forbidden(code, msg string) *Error { return &Error{Kind: KindForbidden, Code: code, Msg: msg} }
func NotFound(code, msg string) *Error { return &Error{Kind: KindNotFound, Code: code, Msg: msg} }
func Conflict(code, msg string) *Error { return &Error{Kind: KindConflict, Code: code, Msg: msg} }
func TooLarge(code, msg string) *Error { return &Error{Kind: KindTooLarge, Code: code, Msg: msg} }
func RateLimited(code, msg string) *Error { return &Error{Kind: KindRateLimited, Code: code, Msg: msg} }
func MethodNotAllowed(code, msg string) *Error {
return &Error{Kind: KindMethodNotAllowed, Code: code, Msg: msg}
}
// Internal verschluckt die Ursache absichtlich: der Nutzer sieht nie einen
// DB- oder Dateisystemfehler (der Interna und Pfade verraten würde), das Log
// sieht ihn immer. Deshalb hat Internal auch keine freie Msg -- eine
// nutzerseitige Unterscheidung zwischen "Datenbankfehler" und "Speichern
// fehlgeschlagen" ist bedeutungslos.
func Internal(cause error) *Error {
return &Error{
Kind: KindInternal,
Code: "internal",
Msg: "Es ist ein Fehler aufgetreten.",
cause: cause,
}
}
// At markiert das Formularfeld, an dem der Fehler hängt. Verkettbar, damit die
// Aufrufstelle einzeilig bleibt: return Invalid(...).At("content")
//
// Achtung: At und WithCause verändern den Empfänger. Auf einen frisch
// konstruierten Fehler anwenden, NICHT auf ein geteiltes Sentinel wie
// errEntryNotFound -- das würde den globalen Wert für alle Requests umschreiben.
func (e *Error) At(field string) *Error {
e.Field = field
return e
}
// WithCause hängt eine technische Ursache an einen fachlichen Fehler. Nützlich,
// wenn ein DB-Fehler fachlich eindeutig ist (UNIQUE-Verletzung -> Conflict),
// die Ursache aber trotzdem ins Log soll.
func (e *Error) WithCause(cause error) *Error {
e.cause = cause
return e
}
// asError normalisiert alles, was kein *Error ist, zu einem Internal.
func asError(err error) *Error {
var e *Error
if errors.As(err, &e) {
return e
}
return Internal(err)
}
// statusOf bildet Kind auf HTTP ab -- die einzige Stelle, an der das passiert.
func statusOf(k Kind) int {
switch k {
case KindInvalid:
return http.StatusBadRequest
case KindUnauth:
return http.StatusUnauthorized
case KindForbidden:
return http.StatusForbidden
case KindNotFound:
return http.StatusNotFound
case KindConflict:
return http.StatusConflict
case KindTooLarge:
return http.StatusRequestEntityTooLarge
case KindRateLimited:
return http.StatusTooManyRequests
case KindMethodNotAllowed:
return http.StatusMethodNotAllowed
default:
return http.StatusInternalServerError
}
}
// logInternal protokolliert nur echte Serverfehler -- Validierungs- und
// Rechtefehler sind normaler Betrieb und würden das Log zumüllen.
func logInternal(e *Error) {
if e.Kind != KindInternal {
return
}
// Die Ursache ist das Einzige, was hier wirklich interessiert; fehlt sie,
// bleibt wenigstens die Meldung.
if e.cause != nil {
log.Printf("internal: %v", e.cause)
} else {
log.Printf("internal: %s", e.Msg)
}
}
// apiErrorBody ist das Drahtformat des JSON-Adapters. Eigener Typ statt einer
// Map, damit die Feldnamen an genau einer Stelle stehen -- sie sind Teil des
// API-Vertrags, sobald ein externer Client existiert.
type apiErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
Field string `json:"field,omitempty"`
Fields map[string]string `json:"fields,omitempty"`
}
// writeAPIError ist der einzige Ausgang für Fehler im JSON-Adapter. Hier -- an
// der Adaptergrenze -- wird auch geloggt, und zwar nur einmal: die
// Service-Schicht loggt nicht, sonst steht derselbe Fehler mehrfach im Log.
func writeAPIError(w http.ResponseWriter, err error) {
e := asError(err)
logInternal(e)
writeJSON(w, statusOf(e.Kind), map[string]any{
"error": apiErrorBody{
Code: e.Code,
Message: e.Msg,
Field: e.Field,
Fields: e.Fields,
},
})
}