[object Object]

← 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

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 →