← back to Cli Printing Press
fix(cli): correct no-auth 403 hints (#629)
013f92adc95676bed9d321e7f9d7758d43c52e11 · 2026-05-05 12:38:11 -0700 · Trevin Chow
Files touched
M internal/generator/generator_test.goM internal/generator/templates/helpers.go.tmplM internal/generator/templates/mcp_tools.go.tmpl
Diff
commit 013f92adc95676bed9d321e7f9d7758d43c52e11
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 5 12:38:11 2026 -0700
fix(cli): correct no-auth 403 hints (#629)
---
internal/generator/generator_test.go | 104 +++++++++++++++++++++++++
internal/generator/templates/helpers.go.tmpl | 3 +
internal/generator/templates/mcp_tools.go.tmpl | 2 +
3 files changed, 109 insertions(+)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index eaaf2c0c..ac20bb17 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1040,6 +1040,110 @@ func TestGenerateWithNoAuth(t *testing.T) {
assert.NoFileExists(t, filepath.Join(outputDir, naming.ValidationBinary("noauth")))
}
+func TestGenerate403HintsFollowAuthMode(t *testing.T) {
+ t.Parallel()
+
+ type snippetCheck struct {
+ want []string
+ reject []string
+ }
+ assertSnippets := func(t *testing.T, body string, check snippetCheck) {
+ t.Helper()
+ for _, want := range check.want {
+ assert.Contains(t, body, want)
+ }
+ for _, reject := range check.reject {
+ assert.NotContains(t, body, reject)
+ }
+ }
+
+ tests := []struct {
+ name string
+ auth spec.AuthConfig
+ helpers snippetCheck
+ mcp snippetCheck
+ }{
+ {
+ name: "no auth",
+ auth: spec.AuthConfig{Type: "none"},
+ helpers: snippetCheck{
+ want: []string{"This API is configured without credentials", "rate limit, geography, bot protection, or endpoint policy"},
+ reject: []string{
+ "Your credentials are valid but lack access",
+ "Check that your API key has the required permissions",
+ },
+ },
+ mcp: snippetCheck{
+ want: []string{"this API is configured without credentials", "rate limit, geography, bot protection, or endpoint policy"},
+ reject: []string{"your credentials are valid but lack access"},
+ },
+ },
+ {
+ name: "api key",
+ auth: spec.AuthConfig{
+ Type: "api_key",
+ Header: "Authorization",
+ Format: "Bearer {token}",
+ EnvVars: []string{"MYAPI_TOKEN"},
+ },
+ helpers: snippetCheck{
+ want: []string{
+ "Your credentials are valid but lack access",
+ "Check that your API key has the required permissions",
+ "Set it with: export MYAPI_TOKEN=<your-key>",
+ },
+ reject: []string{"This API is configured without credentials"},
+ },
+ mcp: snippetCheck{
+ want: []string{
+ "your credentials are valid but lack access",
+ "Set it with: export MYAPI_TOKEN=<your-key>",
+ },
+ reject: []string{"this API is configured without credentials"},
+ },
+ },
+ {
+ name: "oauth2",
+ auth: spec.AuthConfig{
+ Type: "oauth2",
+ EnvVars: []string{"MYAPI_TOKEN"},
+ },
+ helpers: snippetCheck{
+ want: []string{"Your token may lack required scopes", "auth login"},
+ reject: []string{
+ "This API is configured without credentials",
+ "Check that your API key has the required permissions",
+ },
+ },
+ mcp: snippetCheck{
+ want: []string{"your token may lack required scopes", "auth login"},
+ reject: []string{
+ "this API is configured without credentials",
+ "your credentials are valid but lack access",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("auth403" + strings.ReplaceAll(tt.name, " ", ""))
+ apiSpec.Auth = tt.auth
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ helpers := readGeneratedFile(t, outputDir, "internal", "cli", "helpers.go")
+ assertSnippets(t, helpers, tt.helpers)
+
+ tools := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
+ assertSnippets(t, tools, tt.mcp)
+ })
+ }
+}
+
func TestGenerateBrowserChromeTransport(t *testing.T) {
apiSpec := &spec.APISpec{
Name: "websurface",
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 903a6efc..af888476 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -333,6 +333,9 @@ func classifyAPIError(err error) error {
"\n Get a key at: {{.Auth.KeyURL}}"+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
+{{- else if or (eq .Auth.Type "") (eq .Auth.Type "none")}}
+ return authErr(fmt.Errorf("%w\nhint: permission denied. This API is configured without credentials, so the service may be blocking the request by rate limit, geography, bot protection, or endpoint policy."+
+ "\n Run '{{.Name}}-pp-cli doctor' to check connectivity.", err))
{{- else}}
return authErr(fmt.Errorf("%w\nhint: permission denied. Your credentials are valid but lack access to this resource."+
"\n Check that your API key has the required permissions."+
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index faee9c70..62c3d975 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -258,6 +258,8 @@ func makeAPIHandler(method, pathTemplate string, positionalParams []string) serv
return mcplib.NewToolResultError("permission denied: " + {{if and .Auth.Type (ne .Auth.Type "none")}}cliutil.SanitizeErrorBody(msg){{else}}msg{{end}} +
{{- if eq .Auth.Type "oauth2"}}
{{printf "%q" (printf "\nhint: your token may lack required scopes. Re-run '%s-pp-cli auth login' to re-authorize" .Name)}} +
+{{- else if or (eq .Auth.Type "") (eq .Auth.Type "none")}}
+ "\nhint: this API is configured without credentials; the service may be blocking the request by rate limit, geography, bot protection, or endpoint policy." +
{{- else}}
"\nhint: your credentials are valid but lack access to this resource." +
{{- end}}
← 5004e4b0 fix(cli): harden destructive auth dogfood skips (#628)
·
back to Cli Printing Press
·
fix(cli): preserve canonical auth env hints (#633) 6b5945fd →