Remove verb and url from gitops errors (#44555)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #43721 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
Added a Google Calendar integration to gitops .yml with `client_email`
missing from the `api_json_key`.
  - [X] on main, got error: 

```
Error: applying fleet config: PATCH /api/latest/fleet/config received status 422 Validation Failed: client_email is required (API time: 13ms)
```

  - [X] on this branch, got:

```
Error: applying fleet config: Validation Failed: client_email is required (API time: 134ms)
```



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Cleaner CLI error messages: removed extraneous HTTP path/status-code
details from GitOps-related errors, making output easier to read.

* **Tests**
* Added tests to verify the improved error message handling and
nil/non-wrapped error behavior.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/44555)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Scott Gress
2026-05-11 13:48:52 -05:00
committed by GitHub
parent f5c59ae3b4
commit 8b779110a8
4 changed files with 78 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
- Improved errors returned from API when running fleetctl commands by dropping path / status code.
+38
View File
@@ -0,0 +1,38 @@
package fleetctl
import (
"errors"
"regexp"
"github.com/fleetdm/fleet/v4/client"
)
// statusCodeErrPrefixRE matches the "<VERB> <path> received status <code> "
// prefix that BaseClient.ParseResponse adds when wrapping a *StatusCodeErr.
// VERB is uppercase (GET, POST, PATCH, ...) and path starts with "/".
var statusCodeErrPrefixRE = regexp.MustCompile(`[A-Z]+ /\S* received status \d+ `)
// CleanStatusCodeErr returns a copy of err suitable for display in fleetctl
// CLI output. If the chain contains a *client.StatusCodeErr, the verbose
// "VERB /path received status N " prefix added by the HTTP client is stripped
// so users see only the server-provided reason. If no StatusCodeErr is in the
// chain, err is returned unchanged.
//
// The returned value is for printing only — it does not preserve the original
// error chain, so callers that need to inspect err with errors.Is / errors.As
// should do so against the original error before passing it here.
func CleanStatusCodeErr(err error) error {
if err == nil {
return nil
}
var sce *client.StatusCodeErr
if !errors.As(err, &sce) {
return err
}
// Strip the first instance of the status code error prefix from the message, if present.
msg := err.Error()
if loc := statusCodeErrPrefixRE.FindStringIndex(msg); loc != nil {
msg = msg[:loc[0]] + msg[loc[1]:]
}
return errors.New(msg)
}
+37
View File
@@ -0,0 +1,37 @@
package fleetctl
import (
"errors"
"fmt"
"testing"
"github.com/fleetdm/fleet/v4/client"
"github.com/stretchr/testify/require"
)
func TestCleanStatusCodeErr(t *testing.T) {
sce := &client.StatusCodeErr{Code: 422, Body: "Validation Failed: name may not be empty"}
wrapped := fmt.Errorf("PATCH /api/latest/fleet/config received status %w", sce)
outer := fmt.Errorf("applying fleet config: %w", wrapped)
cases := []struct {
name string
in error
want string
}{
{"nil", nil, ""},
{"plain error untouched", errors.New("boom"), "boom"},
{"bare wrapped status code err", wrapped, "Validation Failed: name may not be empty"},
{"outer-wrapped status code err", outer, "applying fleet config: Validation Failed: name may not be empty"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := CleanStatusCodeErr(tc.in)
if tc.in == nil {
require.NoError(t, got)
return
}
require.Equal(t, tc.want, got.Error())
})
}
}
+2 -2
View File
@@ -23,7 +23,7 @@ func main() {
app := fleetctl.CreateApp(os.Stdin, os.Stdout, os.Stderr, exitErrHandler)
fleetctl.StashRawArgs(app, os.Args)
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stdout, "Error: %+v\n", err)
fmt.Fprintf(os.Stdout, "Error: %+v\n", fleetctl.CleanStatusCodeErr(err))
os.Exit(1)
}
}
@@ -34,7 +34,7 @@ func exitErrHandler(c *cli.Context, err error) {
return
}
fmt.Fprintf(c.App.ErrWriter, "Error: %+v\n", err)
fmt.Fprintf(c.App.ErrWriter, "Error: %+v\n", fleetctl.CleanStatusCodeErr(err))
if errors.Is(err, fs.ErrPermission) {
switch runtime.GOOS {