diff --git a/cli/internal/client/http_client.go b/cli/internal/client/http_client.go index 4f69dab..b18058f 100644 --- a/cli/internal/client/http_client.go +++ b/cli/internal/client/http_client.go @@ -276,6 +276,21 @@ func (c *httpClient) do( } if env.Status == 0 { + // json.Unmarshal zeroes missing `status`, so a 5xx whose body + // happens to look like the envelope is indistinguishable from + // success unless we also require a 2xx HTTP status (#6). + if resp.StatusCode/100 != 2 { + switch { + case resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden: + return output.AuthErr("unauthorized", "Run `evercli auth login` to re-authenticate", "") + default: + msg := env.Error + if msg == "" { + msg = http.StatusText(resp.StatusCode) + } + return output.Upstream(resp.StatusCode, msg, env.RequestID) + } + } if out != nil && len(env.Result) > 0 && string(env.Result) != "null" { if err := json.Unmarshal(env.Result, out); err != nil { return output.Internal(fmt.Errorf("decode result: %w", err)) diff --git a/cli/internal/client/http_client_test.go b/cli/internal/client/http_client_test.go index 9f213aa..29b79de 100644 --- a/cli/internal/client/http_client_test.go +++ b/cli/internal/client/http_client_test.go @@ -87,6 +87,25 @@ func TestListAgents_HappyAttachesAuthorization(t *testing.T) { assert.True(t, strings.HasPrefix(auth, "Bearer emk_"), "Bearer header must be set, got %q", auth) } +func TestNon2xxEnvelopeWithZeroStatusIsNotSuccess(t *testing.T) { + // A 500 whose JSON body looks like the success envelope (status omitted + // → unmarshals as 0) used to return err == nil and decode Result (#6). + srv, cli := newTestClient(t) + srv.Handle("POST /agents/list", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"requestId":"req-mock","result":{"items":[]}}`)) + }) + + _, err := cli.ListAgents(context.Background(), client.AgentFilter{}) + require.Error(t, err) + + ce, ok := output.AsCLIError(err) + require.True(t, ok) + assert.Equal(t, output.TypeUpstream, ce.Type) + assert.Equal(t, http.StatusInternalServerError, ce.Code) +} + // (Me / DisconnectAgent tests retired with the slimming pass.) // ---- Auth-space errno classification --------------------------------