From bd5e86dd03f131bcc9f63546c2f1a3392ebea448 Mon Sep 17 00:00:00 2001 From: Kristian Wind Date: Tue, 18 Aug 2026 19:17:49 +0200 Subject: [PATCH] =?UTF-8?q?fix(errors):=20sig=20fejlen=20i=20en=20kode,=20?= =?UTF-8?q?mellemleddet=20ikke=20kaster=20v=C3=A6k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serveren svarede 502 med en forklaring, og Cloudflare erstattede kroppen med sin egen side. Bevist i produktion: handleren skrev 122 bytes JSON på 38 ms med teksten "the panel refused: HTTP 401", og browseren fik en HTML-fejlside. Det gjaldt også Gmail, som bruger samme hjælper — derfor har fladen hele tiden kunnet sige "noget gik galt" og ikke andet, selv om serveren vidste præcis hvad der var sket. En proxy har lov til at gøre det ved en origin-5xx. Så det er ikke Cloudflare, der skal laves om: 502 var den pæne kode, ikke den brugbare. Upstream-afvisninger er nu 422, som går uændret igennem. Navnet StatusUpstreamRefused står ét sted med grunden, så det ikke bliver rullet tilbage af en, der med rette synes 502 lyder mere rigtigt. Fladen læste altid fejlkoden i kroppen og ikke statuskoden, så der var intet at rette dér. Co-Authored-By: Claude Opus 5 --- internal/httpapi/auth_handlers.go | 13 +++++++++++-- internal/httpapi/gmail_test.go | 6 +++--- internal/httpapi/integrations_handlers.go | 4 ++-- internal/httpapi/panel_handlers.go | 2 +- internal/httpapi/panel_test.go | 2 +- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/httpapi/auth_handlers.go b/internal/httpapi/auth_handlers.go index 7ab3ab0..8dce7bf 100644 --- a/internal/httpapi/auth_handlers.go +++ b/internal/httpapi/auth_handlers.go @@ -585,6 +585,15 @@ func inboxName(locale string) string { // sync that has been failing for a week is invisible otherwise. The background job // writes a log line the next restart erases, and nobody is watching the one screen // that would have said so. +// StatusUpstreamRefused is what this server answers when a service it depends on +// says no. Not 502, though that is what it means: a proxy is entitled to replace +// the body of an origin 5xx with a page of its own, and Cloudflare does. That +// swallowed every explanation this server produced — "gmail: 403 Forbidden", +// "the panel refused: HTTP 401" — and left the interface with nothing to show but +// "something went wrong". A 4xx passes through untouched, and the truth reaching +// the person who has to act on it beats the tidier status code. +const StatusUpstreamRefused = http.StatusUnprocessableEntity + func (s *Server) upstream(w http.ResponseWriter, r *http.Request, code, what string, err error) { s.log.Warn(what, "err", err) @@ -593,12 +602,12 @@ func (s *Server) upstream(w http.ResponseWriter, r *http.Request, code, what str userID = u.ID } s.db.RecordError(r.Context(), store.ServerError{ - Method: r.Method, Path: r.URL.Path, Status: http.StatusBadGateway, + Method: r.Method, Path: r.URL.Path, Status: StatusUpstreamRefused, What: what, Message: err.Error(), UserID: userID, RequestID: middleware.GetReqID(r.Context()), }) - writeError(w, http.StatusBadGateway, code, err.Error()) + writeError(w, StatusUpstreamRefused, code, err.Error()) } func (s *Server) internal(w http.ResponseWriter, r *http.Request, what string, err error) { diff --git a/internal/httpapi/gmail_test.go b/internal/httpapi/gmail_test.go index 768f024..afd8e6f 100644 --- a/internal/httpapi/gmail_test.go +++ b/internal/httpapi/gmail_test.go @@ -45,7 +45,7 @@ func TestAGmailFailureSaysWhatGmailSaidAndIsRecorded(t *testing.T) { } resp, body := ts.do(t, "POST", "/api/v1/gmail/sync", nil) - if resp.StatusCode != http.StatusBadGateway { + if resp.StatusCode != StatusUpstreamRefused { t.Fatalf("sync: status %d, want 502 — this is Gmail saying no, not this server breaking", resp.StatusCode) } @@ -68,7 +68,7 @@ func TestAGmailFailureSaysWhatGmailSaidAndIsRecorded(t *testing.T) { if row["what"] != "sync gmail" { t.Errorf("what = %v, want \"sync gmail\"", row["what"]) } - if row["status"] != float64(http.StatusBadGateway) { + if row["status"] != float64(StatusUpstreamRefused) { t.Errorf("status = %v, want 502", row["status"]) } } @@ -143,7 +143,7 @@ func TestASlowMailboxDoesNotHangTheRequest(t *testing.T) { t.Errorf("the sync took %s; the budget is not being applied", took.Round(time.Second)) } - if resp.StatusCode >= 500 && resp.StatusCode != http.StatusBadGateway { + if resp.StatusCode >= 500 && resp.StatusCode != StatusUpstreamRefused { t.Errorf("status %d — a slow mailbox is not this server breaking", resp.StatusCode) } t.Logf("came back in %s with %d", took.Round(time.Millisecond), resp.StatusCode) diff --git a/internal/httpapi/integrations_handlers.go b/internal/httpapi/integrations_handlers.go index 008407d..422f69b 100644 --- a/internal/httpapi/integrations_handlers.go +++ b/internal/httpapi/integrations_handlers.go @@ -288,7 +288,7 @@ func (s *Server) handleAISplit(w http.ResponseWriter, r *http.Request) { subtasks, err := ai.New(cfg).SplitIntoSubtasks(ctx, task.Content, task.Description, user.Locale) if err != nil { s.log.Warn("ai split", "err", err) - writeError(w, http.StatusBadGateway, CodeInternal, err.Error()) + writeError(w, StatusUpstreamRefused, CodeInternal, err.Error()) return } @@ -352,7 +352,7 @@ func (s *Server) handleAISummary(w http.ResponseWriter, r *http.Request) { summary, err := ai.New(cfg).WeeklySummary(ctx, lines, user.Locale) if err != nil { s.log.Warn("ai summary", "err", err) - writeError(w, http.StatusBadGateway, CodeInternal, err.Error()) + writeError(w, StatusUpstreamRefused, CodeInternal, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"summary": summary}) diff --git a/internal/httpapi/panel_handlers.go b/internal/httpapi/panel_handlers.go index 06166f5..dfc5eb2 100644 --- a/internal/httpapi/panel_handlers.go +++ b/internal/httpapi/panel_handlers.go @@ -96,7 +96,7 @@ func (s *Server) handleRestartFromPanel(w http.ResponseWriter, r *http.Request) if resp.StatusCode >= 400 { s.log.Error("panel refused the restart", "status", resp.StatusCode) - writeError(w, http.StatusBadGateway, CodeInternal, + writeError(w, StatusUpstreamRefused, CodeInternal, fmt.Sprintf("the panel refused: HTTP %d — check VERDANDE_PANEL_TOKEN has control of this server", resp.StatusCode)) return } diff --git a/internal/httpapi/panel_test.go b/internal/httpapi/panel_test.go index 57f0baf..fd2bb7a 100644 --- a/internal/httpapi/panel_test.go +++ b/internal/httpapi/panel_test.go @@ -68,7 +68,7 @@ func TestAPanelThatRefusesSaysSoAsJSON(t *testing.T) { ts.bootstrap(t) resp, body := ts.do(t, "POST", "/api/v1/panel/restart", nil) - if resp.StatusCode != http.StatusBadGateway { + if resp.StatusCode != StatusUpstreamRefused { t.Errorf("status %d, want 502: %s", resp.StatusCode, body) } if msg, _ := body["error"].(string); !strings.Contains(msg, "403") {