Compare commits

...
Author SHA1 Message Date
patrickandClaude Sonnet 5.5 87f3cda5ad fix(dms): apierror.Normalize verbirgt 5xx-Texte (QA-15 F-14)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
dms/internal/apierror gab bei Status >= 500 den vom Handler geschriebenen
Text unveraendert an den Aufrufer (z. B. DB-/Pfadfehler). Angleichung an
internal/apierror: 5xx liefert "Interner Fehler" mit correlation_id, der
Originaltext geht nur ins Server-Log. 4xx unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 13:39:38 +01:00
3 changed files with 74 additions and 8 deletions

No files matched your search

+34 -7
View File
@@ -3,7 +3,10 @@ package apierror
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"strings"
)
@@ -45,8 +48,9 @@ func CodeForStatus(status int) string {
type body struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Code string `json:"code"`
Message string `json:"message"`
CorrelationID string `json:"correlation_id,omitempty"`
} `json:"error"`
}
@@ -59,13 +63,22 @@ func Write(w http.ResponseWriter, status int, code, message string) {
_ = json.NewEncoder(w).Encode(b)
}
func newCorrelationID() string {
buf := make([]byte, 8)
if _, err := rand.Read(buf); err != nil {
return "korrelations-id-nicht-verfuegbar"
}
return hex.EncodeToString(buf)
}
// Normalize schreibt jede Fehlerantwort (Status >= 400) im Core-Format um;
// Erfolgsantworten laufen ungepuffert durch.
// Erfolgsantworten laufen ungepuffert durch. 5xx-Texte werden nie an den
// Aufrufer gegeben (Interna-Leck, QA-15 F-14), nur mit Korrelations-ID geloggt.
func Normalize(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &recorder{ResponseWriter: w}
next.ServeHTTP(rec, r)
rec.finish()
rec.finish(r)
})
}
@@ -106,13 +119,27 @@ func (r *recorder) Flush() {
func (r *recorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
func (r *recorder) finish() {
func (r *recorder) finish(req *http.Request) {
if !r.buffered {
return
}
out := map[string]any{}
message := strings.TrimSpace(r.buf.String())
code := CodeForStatus(r.status)
rawMessage := strings.TrimSpace(r.buf.String())
if r.status >= 500 {
corrID := newCorrelationID()
log.Printf("apierror: 5xx %s %s status=%d korrelations-id=%s ursprung=%q", req.Method, req.URL.Path, r.status, corrID, rawMessage)
out := body{}
out.Error.Code = code
out.Error.Message = "Interner Fehler"
out.Error.CorrelationID = corrID
encoded, _ := json.Marshal(out)
r.flush(append(encoded, '\n'))
return
}
out := map[string]any{}
message := rawMessage
var parsed map[string]any
if json.Unmarshal(r.buf.Bytes(), &parsed) == nil && parsed != nil {
+1 -1
View File
@@ -43,7 +43,7 @@ func TestNormalizeRewritesLegacyErrorShapes(t *testing.T) {
_, _ = w.Write([]byte(`{"error":"ungueltiger wert"}`))
}, 422, "unprocessable", "ungueltiger wert"},
{"leerer body", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(403) }, 403, "forbidden", "Forbidden"},
{"5xx", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", 500) }, 500, "internal_error", "boom"},
{"5xx", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", 500) }, 500, "internal_error", "Interner Fehler"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
+39
View File
@@ -0,0 +1,39 @@
package apierror
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestNormalize5xxHidesInternalsAndSetsCorrelationID(t *testing.T) {
const secret = "pq: connection refused to db-internal:5432 /var/lib/dms/secret.key"
rec := httptest.NewRecorder()
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, secret, http.StatusInternalServerError)
})).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
if strings.Contains(rec.Body.String(), "db-internal") || strings.Contains(rec.Body.String(), "secret.key") {
t.Fatalf("5xx-Interna im Body: %q", rec.Body.String())
}
e, _ := decode(t, rec)["error"].(map[string]any)
if id, _ := e["correlation_id"].(string); id == "" {
t.Errorf("correlation_id fehlt: %v", e)
}
}
func TestNormalize4xxKeepsMessageWithoutCorrelationID(t *testing.T) {
rec := httptest.NewRecorder()
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "dokument nicht gefunden", http.StatusNotFound)
})).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
e, _ := decode(t, rec)["error"].(map[string]any)
if _, has := e["correlation_id"]; has {
t.Errorf("4xx darf keine correlation_id tragen: %v", e)
}
if msg, _ := e["message"].(string); msg != "dokument nicht gefunden" {
t.Errorf("message = %q", msg)
}
}