Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f3cda5ad |
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user