Skip to content

Commit ebbbb50

Browse files
committed
Fix exit codes, 100MB truncation, go.mod, minor robustness
Exit codes: - SetFlagErrorFunc wraps cobra flag errors as UsageError (exit 2) - isCobraUsageError detects arg-count and unknown-command errors (exit 2) - Both now emit code=usage_error in JSON instead of api_error API client: - Detect 100MB truncation and return clear ApiError instead of letting downstream JSON parsing fail with cryptic 'unexpected end of JSON input' config_unix.go: - Check partial write: return error if fewer bytes written than expected errors_test.go: - Use comma-ok type assertion to give clearer failure message go.mod: - go mod tidy: briandowns/spinner and tidwall/pretty no longer // indirect homebrew-tap formula: - Inject version via ldflags -X .../pkg/version.Version=#{version} so installs show the actual version instead of 'dev'
1 parent 0fe62cd commit ebbbb50

6 files changed

Lines changed: 39 additions & 6 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ go 1.25.0
44

55
require (
66
github.com/BurntSushi/toml v1.6.0
7+
github.com/briandowns/spinner v1.23.2
78
github.com/itchyny/gojq v0.12.18
89
github.com/spf13/cobra v1.10.2
10+
github.com/tidwall/pretty v1.2.1
911
golang.org/x/term v0.41.0
1012
)
1113

1214
require (
13-
github.com/briandowns/spinner v1.23.2 // indirect
1415
github.com/fatih/color v1.7.0 // indirect
1516
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1617
github.com/itchyny/timefmt-go v0.1.7 // indirect
1718
github.com/mattn/go-colorable v0.1.2 // indirect
1819
github.com/mattn/go-isatty v0.0.20 // indirect
1920
github.com/spf13/pflag v1.0.9 // indirect
20-
github.com/tidwall/pretty v1.2.1 // indirect
2121
golang.org/x/sys v0.42.0 // indirect
2222
)

pkg/api/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,14 @@ func (c *Client) doGet(endpoint string, params map[string]string) ([]byte, error
5959
}
6060
defer resp.Body.Close()
6161

62-
body, err := io.ReadAll(io.LimitReader(resp.Body, 100<<20)) // 100 MB limit
62+
const maxBytes = 100 << 20 // 100 MB
63+
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes))
6364
if err != nil {
6465
return nil, &clierrors.NetworkError{Message: "Failed to read response: " + err.Error()}
6566
}
67+
if len(body) == maxBytes {
68+
return nil, &clierrors.ApiError{Message: "Response exceeded 100 MB limit"}
69+
}
6670

6771
if resp.StatusCode != http.StatusOK {
6872
// Try to extract error message from JSON body

pkg/cmd/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ func init() {
4747
rootCmd.PersistentFlags().StringVar(&apiKeyFlag, "api-key", "", "SerpApi API key (env: SERPAPI_KEY)")
4848
rootCmd.PersistentFlags().StringVar(&fieldsFlag, "fields", "", "Restrict JSON fields (server-side json_restrictor)")
4949
rootCmd.PersistentFlags().StringVar(&jqFlag, "jq", "", "Apply jq filter to output")
50+
51+
// Wrap cobra flag-parsing errors as UsageError so they get exit code 2.
52+
rootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
53+
return &clierrors.UsageError{Message: err.Error()}
54+
})
5055
}
5156

5257
// resolveAPIKey merges --api-key flag with SERPAPI_KEY env var, then config file.

pkg/config/config_unix.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package config
44

55
import (
6+
"fmt"
67
"os"
78
)
89

@@ -16,6 +17,12 @@ func writeFileSecure(path string, data []byte) error {
1617
return err
1718
}
1819
defer f.Close()
19-
_, err = f.Write(data)
20-
return err
20+
n, err := f.Write(data)
21+
if err != nil {
22+
return err
23+
}
24+
if n != len(data) {
25+
return fmt.Errorf("incomplete write: wrote %d of %d bytes", n, len(data))
26+
}
27+
return nil
2128
}

pkg/errors/errors.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ func ExitCode(err error) int {
3333
case *NetworkError:
3434
return 1
3535
default:
36+
if isCobraUsageError(err) {
37+
return 2
38+
}
3639
return 1
3740
}
3841
}
@@ -46,10 +49,21 @@ func errorCode(err error) string {
4649
case *NetworkError:
4750
return "network_error"
4851
default:
52+
if isCobraUsageError(err) {
53+
return "usage_error"
54+
}
4955
return "api_error"
5056
}
5157
}
5258

59+
// isCobraUsageError detects cobra argument-validation errors by message pattern.
60+
func isCobraUsageError(err error) bool {
61+
msg := err.Error()
62+
return strings.HasPrefix(msg, "accepts ") ||
63+
strings.HasPrefix(msg, "unknown command") ||
64+
strings.HasPrefix(msg, "required flag")
65+
}
66+
5367
// PrintError writes a structured JSON error to stderr.
5468
func PrintError(err error) {
5569
code := errorCode(err)

pkg/errors/errors_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ func TestPrintErrorUsageError(t *testing.T) {
7373
if err := json.Unmarshal([]byte(output), &parsed); err != nil {
7474
t.Fatalf("failed to parse error JSON: %v\noutput: %s", err, output)
7575
}
76-
errObj := parsed["error"].(map[string]any)
76+
errObj, ok := parsed["error"].(map[string]any)
77+
if !ok {
78+
t.Fatalf("expected error object, got %T", parsed["error"])
79+
}
7780
if errObj["code"] != "usage_error" {
7881
t.Errorf("expected code usage_error, got %v", errObj["code"])
7982
}

0 commit comments

Comments
 (0)