← back to Cli Printing Press
fix(cli): emit default Accept */* header in generated HTTP clients (#1112)
c626efca9403e829bafbee3c160ce7354c47d3f4 · 2026-05-11 13:56:58 -0700 · Trevin Chow
* fix(cli): emit default Accept */* header in generated HTTP clients
Go's net/http omits Accept by default; browsers, curl, and other stdlibs
always send it. Fingerprint-checking WAFs (Imperva, Akamai, Cloudflare
bot-mode, DataDome) flag the absence as a bot signal and answer with
empty-body HTTP 500s, breaking every reverse-engineered consumer API a
printed CLI tries to talk to.
The generator now mirrors the existing User-Agent default in
client.go.tmpl's do() method: an if-empty guard sets Accept to "*/*"
only when nothing earlier in the request pipeline already set it.
Per-endpoint headerOverrides, spec-declared RequiredHeaders, and
content-negotiated overrides all win because they run before the guard.
The fallback is gated by UsesBrowserManagedUserAgent so surf's Chrome
impersonation transports keep managing the full header bundle.
Closes #920
* docs(cli): broaden WAF response-code description in Accept-default comment
Greptile review flagged that the inline comment overspecified the
WAF response code. The Resy case (issue #920 origin) actually
returned empty-body HTTP 500, but Imperva, Akamai, Cloudflare
bot-mode, and DataDome more commonly return 403 or a JS-challenge
redirect depending on vendor and rule tier. The new wording covers
all three so future readers tracing the guard's rationale aren't
misled.
Files touched
M internal/generator/generator_test.goM internal/generator/templates/client.go.tmplM testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
Diff
commit c626efca9403e829bafbee3c160ce7354c47d3f4
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon May 11 13:56:58 2026 -0700
fix(cli): emit default Accept */* header in generated HTTP clients (#1112)
* fix(cli): emit default Accept */* header in generated HTTP clients
Go's net/http omits Accept by default; browsers, curl, and other stdlibs
always send it. Fingerprint-checking WAFs (Imperva, Akamai, Cloudflare
bot-mode, DataDome) flag the absence as a bot signal and answer with
empty-body HTTP 500s, breaking every reverse-engineered consumer API a
printed CLI tries to talk to.
The generator now mirrors the existing User-Agent default in
client.go.tmpl's do() method: an if-empty guard sets Accept to "*/*"
only when nothing earlier in the request pipeline already set it.
Per-endpoint headerOverrides, spec-declared RequiredHeaders, and
content-negotiated overrides all win because they run before the guard.
The fallback is gated by UsesBrowserManagedUserAgent so surf's Chrome
impersonation transports keep managing the full header bundle.
Closes #920
* docs(cli): broaden WAF response-code description in Accept-default comment
Greptile review flagged that the inline comment overspecified the
WAF response code. The Resy case (issue #920 origin) actually
returned empty-body HTTP 500, but Imperva, Akamai, Cloudflare
bot-mode, and DataDome more commonly return 403 or a JS-challenge
redirect depending on vendor and rule tier. The new wording covers
all three so future readers tracing the guard's rationale aren't
misled.
---
internal/generator/generator_test.go | 30 ++++++++++++++++++++++
internal/generator/templates/client.go.tmpl | 8 ++++++
.../internal/client/client.go | 8 ++++++
.../internal/client/client.go | 8 ++++++
.../tier-routing-golden/internal/client/client.go | 8 ++++++
5 files changed, 62 insertions(+)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 17e07ff4..4680161a 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1496,6 +1496,10 @@ func TestGenerateBrowserChromeH3Transport(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, string(clientGo), `"github.com/enetx/surf"`)
assert.Contains(t, string(clientGo), "ForceHTTP3()")
+ // surf's Chrome impersonation manages the Accept header alongside
+ // User-Agent on the H3 transport too; the generator must not emit a
+ // competing default.
+ assert.NotContains(t, string(clientGo), `req.Header.Set("Accept", "*/*")`)
runGoCommand(t, outputDir, "mod", "tidy")
runGoCommand(t, outputDir, "test", "./internal/client")
@@ -5768,6 +5772,8 @@ func TestGenerate_UserAgentOverrideGatedByBrowserTransport(t *testing.T) {
standardClient, err := os.ReadFile(filepath.Join(standardDir, "internal", "client", "client.go"))
require.NoError(t, err)
assert.Contains(t, string(standardClient), `req.Header.Set("User-Agent", "standard-pp-cli/0.1.0")`)
+ assert.Contains(t, string(standardClient), `req.Header.Set("Accept", "*/*")`)
+ assert.Contains(t, string(standardClient), `if req.Header.Get("Accept") == "" {`)
browserDir := filepath.Join(t.TempDir(), "browser-pp-cli")
browserSpec := baseSpec("browser")
@@ -5776,6 +5782,9 @@ func TestGenerate_UserAgentOverrideGatedByBrowserTransport(t *testing.T) {
browserClient, err := os.ReadFile(filepath.Join(browserDir, "internal", "client", "client.go"))
require.NoError(t, err)
assert.NotContains(t, string(browserClient), `req.Header.Set("User-Agent"`)
+ // surf's Chrome impersonation manages the Accept header alongside
+ // User-Agent; the generator must not emit a competing default for either.
+ assert.NotContains(t, string(browserClient), `req.Header.Set("Accept", "*/*")`)
// auth.go is not emitted for auth.type:none specs (see Generator.renderAuthFiles).
// Stronger assertion than the previous "no newAuthRefreshCmd in auth.go": there's
// no auth.go at all for no-auth CLIs.
@@ -5783,6 +5792,27 @@ func TestGenerate_UserAgentOverrideGatedByBrowserTransport(t *testing.T) {
assert.True(t, os.IsNotExist(err), "auth.go should not be emitted for auth.type:none specs")
}
+func TestGenerateRequiredAcceptHeaderBeatsDefaultAccept(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("acceptoverride")
+ apiSpec.RequiredHeaders = []spec.RequiredHeader{
+ {Name: "Accept", Value: "application/vnd.api.v3+json"},
+ }
+ apiSpec.Auth.VerifyPath = "/items"
+
+ outputDir := filepath.Join(t.TempDir(), "acceptoverride-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ clientSrc := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+ requiredIdx := strings.Index(clientSrc, `req.Header.Set("Accept", "application/vnd.api.v3+json")`)
+ defaultIdx := strings.Index(clientSrc, `req.Header.Set("Accept", "*/*")`)
+ require.GreaterOrEqual(t, requiredIdx, 0, "spec-required Accept header must be emitted")
+ require.GreaterOrEqual(t, defaultIdx, 0, "Accept fallback default must still be emitted")
+ assert.Less(t, requiredIdx, defaultIdx, "spec-required Accept must be set before the if-empty default")
+ assert.Contains(t, clientSrc, `if req.Header.Get("Accept") == "" {`)
+}
+
func TestGenerateRequiredUserAgentHeaderBeatsDefaultUserAgent(t *testing.T) {
t.Parallel()
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 638dbfce..78d632f3 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -863,6 +863,14 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
}
+ // Go's net/http omits Accept by default; browsers, curl, and other
+ // stdlibs always send it. Fingerprint-checking WAFs (Imperva, Akamai,
+ // Cloudflare bot-mode, DataDome) flag the absence as a bot signal
+ // and answer with empty-body 5xx, 403, or a challenge redirect
+ // depending on vendor and rule tier.
+ if req.Header.Get("Accept") == "" {
+ req.Header.Set("Accept", "*/*")
+ }
{{- end}}
resp, err := c.HTTPClient.Do(req)
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
index adf20abe..496b28e3 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
@@ -253,6 +253,14 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "printing-press-oauth2-pp-cli/1.0.0")
}
+ // Go's net/http omits Accept by default; browsers, curl, and other
+ // stdlibs always send it. Fingerprint-checking WAFs (Imperva, Akamai,
+ // Cloudflare bot-mode, DataDome) flag the absence as a bot signal
+ // and answer with empty-body 5xx, 403, or a challenge redirect
+ // depending on vendor and rule tier.
+ if req.Header.Get("Accept") == "" {
+ req.Header.Set("Accept", "*/*")
+ }
resp, err := c.HTTPClient.Do(req)
if err != nil {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
index 47cf766f..dffda2dc 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
@@ -247,6 +247,14 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
}
+ // Go's net/http omits Accept by default; browsers, curl, and other
+ // stdlibs always send it. Fingerprint-checking WAFs (Imperva, Akamai,
+ // Cloudflare bot-mode, DataDome) flag the absence as a bot signal
+ // and answer with empty-body 5xx, 403, or a challenge redirect
+ // depending on vendor and rule tier.
+ if req.Header.Get("Accept") == "" {
+ req.Header.Set("Accept", "*/*")
+ }
resp, err := c.HTTPClient.Do(req)
if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
index feb16071..c855a5d1 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
@@ -350,6 +350,14 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
}
+ // Go's net/http omits Accept by default; browsers, curl, and other
+ // stdlibs always send it. Fingerprint-checking WAFs (Imperva, Akamai,
+ // Cloudflare bot-mode, DataDome) flag the absence as a bot signal
+ // and answer with empty-body 5xx, 403, or a challenge redirect
+ // depending on vendor and rule tier.
+ if req.Header.Get("Accept") == "" {
+ req.Header.Set("Accept", "*/*")
+ }
resp, err := c.HTTPClient.Do(req)
if err != nil {
← c6420747 fix(cli): honor Spec.BasePath in generated client URL constr
·
back to Cli Printing Press
·
fix(cli): infer pagination defaults from plain params; prese 10cc48a8 →