From cf2885ece46d7d8cf070fca60bc7a85878f8151d Mon Sep 17 00:00:00 2001 From: geobelsky Date: Tue, 24 Mar 2026 10:21:29 +0000 Subject: [PATCH 1/2] Replace raw HTTP error leakage with user-friendly messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 8 sites that exposed raw HTTP response bodies to users with httpErrorMessage() which translates status codes to actionable messages (401→"Run axme login", 403→"Permission denied", 429→quota details, etc.) - Fix printResult to show user-friendly errors instead of dumping raw JSON in non-JSON mode for error responses (status >= 400) - Clarify --bearer-token help text as alias for --actor-token All tests pass. --- cmd/axme/main.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/cmd/axme/main.go b/cmd/axme/main.go index 4621ba3..ec67f93 100644 --- a/cmd/axme/main.go +++ b/cmd/axme/main.go @@ -162,7 +162,7 @@ func buildRoot(rt *runtime) *cobra.Command { cmd.PersistentFlags().StringVar(&rt.overrideBase, "base-url", "", "gateway base URL override") cmd.PersistentFlags().StringVar(&rt.overrideKey, "api-key", "", "gateway API key override") cmd.PersistentFlags().StringVar(&rt.overrideJWT, "actor-token", "", "actor token override") - cmd.PersistentFlags().StringVar(&rt.overrideJWT, "bearer-token", "", "bearer token override") + cmd.PersistentFlags().StringVar(&rt.overrideJWT, "bearer-token", "", "bearer token override (alias for --actor-token)") cmd.PersistentFlags().StringVar(&rt.overrideOrg, "org-id", "", "default org id override") cmd.PersistentFlags().StringVar(&rt.overrideWs, "workspace-id", "", "default workspace id override") cmd.PersistentFlags().StringVar(&rt.overrideOwn, "owner-agent", "", "owner agent override") @@ -2823,7 +2823,7 @@ func newStatusCmd(rt *runtime) *cobra.Command { return rt.printJSON(map[string]any{"status_code": status, "ok": status < 400, "body": body}) } if status >= 400 { - return fmt.Errorf("gateway returned %d: %s", status, raw) + return &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } svc := asString(body["service"]) if svc == "" { @@ -3529,7 +3529,7 @@ func (rt *runtime) personalContextFromServer(ctx context.Context, c *clientConfi case status == 404 && strings.Contains(strings.ToLower(detail), "not bound to an organization/workspace context"): return nil, &cliError{Code: 2, Msg: personalContextRequirementMessage(detail)} } - return nil, fmt.Errorf("personal context returned %d: %s", status, raw) + return nil, &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } return body, nil } @@ -3889,7 +3889,7 @@ func (rt *runtime) personalWorkspaceSelectionAPIError(status int, body map[strin case status == 403 && (errorCode == "invalid_actor_scope" || strings.Contains(strings.ToLower(detail), "actor identity")): return &cliError{Code: 2, Msg: personalContextRequirementMessage(detail)} } - return fmt.Errorf("workspace selection returned %d: %s", status, raw) + return &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } func enterpriseMemberRequirementMessage(_ string) string { @@ -3930,7 +3930,7 @@ func (rt *runtime) enterpriseMembersAPIError(status int, body map[string]any, ra case status == 429: return &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} default: - return fmt.Errorf("enterprise member request returned %d: %s", status, raw) + return &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } } @@ -3962,7 +3962,7 @@ func (rt *runtime) serviceAccountsAPIError(status int, body map[string]any, raw case status == 429: return &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} default: - return fmt.Errorf("service account request returned %d: %s", status, raw) + return &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } } @@ -4115,7 +4115,7 @@ func (rt *runtime) listAccountSessions(ctx context.Context, c *clientConfig, inc return nil, err } if status >= 400 { - return nil, fmt.Errorf("list sessions returned %d: %s", status, raw) + return nil, &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } items := asSlice(body["sessions"]) out := make([]map[string]any, 0, len(items)) @@ -4139,7 +4139,7 @@ func (rt *runtime) revokeAccountSessionByID(ctx context.Context, c *clientConfig return false, err } if status >= 400 { - return false, fmt.Errorf("revoke session returned %d: %s", status, raw) + return false, &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } return asBool(body["revoked"]) || asBool(body["ok"]), nil } @@ -4180,7 +4180,7 @@ func (rt *runtime) logoutAllAccountSessions(ctx context.Context, c *clientConfig return 0, err } if status >= 400 { - return status, fmt.Errorf("logout-all returned %d: %s", status, raw) + return status, &cliError{Code: 1, Msg: httpErrorMessage(status, raw)} } if !asBool(body["ok"]) { return status, fmt.Errorf("logout-all did not confirm success") @@ -4444,11 +4444,13 @@ func (rt *runtime) printResult(status int, body any) error { if rt.outputJSON { return rt.printJSON(result) } - raw, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(raw)) if status >= 400 { - return &cliError{Code: 1, Msg: "request failed"} + // Extract user-friendly error from the response body + rawBytes, _ := json.Marshal(body) + return &cliError{Code: 1, Msg: httpErrorMessage(status, string(rawBytes))} } + raw, _ := json.MarshalIndent(result, "", " ") + fmt.Println(string(raw)) return nil } From e1539308c1455ea6fd34eee4a184e01f35e2a1d3 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Tue, 24 Mar 2026 10:31:29 +0000 Subject: [PATCH 2/2] Handle FastAPI validation error arrays in httpErrorMessage Detail field in error responses can be either a string or an array of validation error objects. Parse both formats to extract user-friendly message instead of showing "Request failed (422)." with no context. Before: "Request failed (422)." After: "Request failed (422): Input should be a valid UUID, ..." --- cmd/axme/main.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/cmd/axme/main.go b/cmd/axme/main.go index ec67f93..b94bb9a 100644 --- a/cmd/axme/main.go +++ b/cmd/axme/main.go @@ -3460,10 +3460,28 @@ func httpErrorMessage(status int, raw string) string { ResetAt string `json:"reset_at"` } `json:"details"` } `json:"error"` - Detail string `json:"detail"` + Detail json.RawMessage `json:"detail"` } _ = json.Unmarshal([]byte(raw), &parsed) + // Extract detail as string — handles both string and array (FastAPI validation errors). + var detailStr string + if len(parsed.Detail) > 0 { + // Try as string first. + var s string + if json.Unmarshal(parsed.Detail, &s) == nil { + detailStr = s + } else { + // Try as array of validation errors [{msg: "..."}]. + var arr []struct { + Msg string `json:"msg"` + } + if json.Unmarshal(parsed.Detail, &arr) == nil && len(arr) > 0 { + detailStr = arr[0].Msg + } + } + } + code := parsed.Error.Code switch { case status == 401 && (code == "missing_actor_token" || code == "missing_api_key" || code == "unauthorized"): @@ -3475,7 +3493,7 @@ func httpErrorMessage(status int, raw string) string { case status == 404: msg := parsed.Error.Message if msg == "" { - msg = parsed.Detail + msg = detailStr } if msg != "" { return fmt.Sprintf("Not found: %s", msg) @@ -3499,8 +3517,8 @@ func httpErrorMessage(status int, raw string) string { if parsed.Error.Message != "" { return fmt.Sprintf("Request failed (%d): %s", status, parsed.Error.Message) } - if parsed.Detail != "" { - return fmt.Sprintf("Request failed (%d): %s", status, parsed.Detail) + if detailStr != "" { + return fmt.Sprintf("Request failed (%d): %s", status, detailStr) } return fmt.Sprintf("Request failed (%d).", status) }