Skip to content

Commit 400a2ed

Browse files
🐛 fix(crux): /v1/health 503 body names the missing tmux through api.Client
api.Client.checkStatus decodes a non-2xx body as api.Error; the tmux 503 body was a plain api.Health with no error key, so StatusError.Error() fell back to "crux server returned HTTP 503" and the supervisor polled to ReadyTimeout, then killed and restarted crux serve without ever logging that tmux was the cause. - api.Health gains error/code (omitempty) sharing api.Error's wire keys, so the 503 body decodes as both. - handleHealth sets error="tmux not found on PATH: ..." and code=unavailable (new api.CodeUnavailable) on the 503 path; the 200 body is unchanged. - Tests: serve asserts the 503 body's error/code; api asserts Client.Health surfaces the message in the StatusError and that WaitReady keeps polling a 503 until ctx ends. Hive-Fix-Attempt: 1/6 Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com>
1 parent 5afc5e6 commit 400a2ed

5 files changed

Lines changed: 51 additions & 1 deletion

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
- Crux `GET /v1/health` now reports a `tmux` field (where tmux resolves on PATH) and answers 503, with the full health body, when tmux is missing: every agent runs in tmux, so a server without it cannot serve traffic and was reporting healthy ([hivecommons/crux#137](https://github.com/hivecommons/crux/issues/137)). Existing fields are unchanged; probes that treated any 200 as healthy now see the failure.
1+
- Crux `GET /v1/health` now reports a `tmux` field (where tmux resolves on PATH) and answers 503, with the full health body, when tmux is missing (the body also carries `error`/`code`, so `api.Client` callers and the supervisor see "tmux not found on PATH" rather than a bare 503): every agent runs in tmux, so a server without it cannot serve traffic and was reporting healthy ([hivecommons/crux#137](https://github.com/hivecommons/crux/issues/137)). Existing fields are unchanged; probes that treated any 200 as healthy now see the failure.

‎src/crux/api/api.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ type Health struct {
108108
// Tmux is where tmux resolves on PATH, "" when it does not. Every agent
109109
// runs inside tmux, so the route answers 503 (with this body) when empty.
110110
Tmux string `json:"tmux"`
111+
// Error and Code are set only on a non-2xx answer and share their wire
112+
// keys with api.Error, so the 503 body also decodes as the Error that
113+
// Client surfaces in a *StatusError: a caller polling health (the
114+
// supervisor's WaitReady) sees "tmux not found on PATH", not a bare 503.
115+
Error string `json:"error,omitempty"`
116+
Code string `json:"code,omitempty"`
111117
}
112118

113119
// HiveCallback is the wire form of the callback client's status.
@@ -257,4 +263,7 @@ const (
257263
// holds; the runtime does not replace definitions.
258264
CodeConflict = "conflict"
259265
CodeRuntime = "runtime_error"
266+
// CodeUnavailable is a server that is up but cannot serve: GET /v1/health
267+
// answers 503 with it when tmux is missing from PATH.
268+
CodeUnavailable = "unavailable"
260269
)

‎src/crux/api/client_test.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,36 @@ func TestClientStatusTracksReachability(t *testing.T) {
512512
t.Fatalf("status after success = %+v", st)
513513
}
514514
}
515+
516+
// A 503 health body carries error/code alongside the health fields, so the
517+
// client names the cause (the supervisor's WaitReady logs this error).
518+
func TestClientHealthUnavailableNamesCause(t *testing.T) {
519+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
520+
w.Header().Set("Content-Type", ContentTypeJSON)
521+
w.WriteHeader(http.StatusServiceUnavailable)
522+
_ = json.NewEncoder(w).Encode(Health{
523+
Version: Version,
524+
Error: "tmux not found on PATH: exec: \"tmux\": executable file not found in $PATH",
525+
Code: CodeUnavailable,
526+
})
527+
}))
528+
t.Cleanup(srv.Close)
529+
c, err := NewClient(srv.URL, "tok")
530+
if err != nil {
531+
t.Fatal(err)
532+
}
533+
_, err = c.Health(context.Background())
534+
var se *StatusError
535+
if !errors.As(err, &se) || se.Status != http.StatusServiceUnavailable || se.Body.Code != CodeUnavailable {
536+
t.Fatalf("status error: %+v (%v)", se, err)
537+
}
538+
if !strings.Contains(err.Error(), "tmux not found on PATH") {
539+
t.Fatalf("error does not name tmux: %v", err)
540+
}
541+
// WaitReady keeps polling a 503 (it is not 401) and ends with ctx.
542+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
543+
defer cancel()
544+
if err := c.WaitReady(ctx, time.Millisecond); !errors.Is(err, context.DeadlineExceeded) {
545+
t.Fatalf("wait ready: %v", err)
546+
}
547+
}

‎src/crux/serve/health_test.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"os/exec"
77
"path/filepath"
8+
"strings"
89
"testing"
910

1011
"github.com/hivecommons/hive/crux/api"
@@ -28,6 +29,10 @@ func TestHealthTmuxMissingIs503(t *testing.T) {
2829
if h.Tmux != "" || h.Version != api.Version {
2930
t.Fatalf("health: %+v", h)
3031
}
32+
// The body is also an api.Error, so api.Client callers see the cause.
33+
if h.Code != api.CodeUnavailable || !strings.Contains(h.Error, "tmux not found on PATH") {
34+
t.Fatalf("health error: %+v", h)
35+
}
3136
}
3237

3338
func TestHealthTmuxPresentIs200(t *testing.T) {

‎src/crux/serve/serve.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,10 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
391391
}
392392
status := http.StatusOK
393393
if path, err := tmux.LookPath(); err != nil {
394+
// The body doubles as an api.Error so api.Client reports the cause.
394395
status = http.StatusServiceUnavailable
396+
h.Error = fmt.Sprintf("tmux not found on PATH: %v", err)
397+
h.Code = api.CodeUnavailable
395398
} else {
396399
h.Tmux = path
397400
}

0 commit comments

Comments
 (0)