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") {