← back to Cli Printing Press
fix(cli): preserve browser-sniffed request defaults (#737)
8721df4bf310cac7b0619747af9db366508583c3 · 2026-05-08 11:58:39 -0700 · Trevin Chow
Files touched
M internal/catalog/catalog.goM internal/cli/generate_test.goM internal/cli/root.goM internal/generator/generator_test.goM internal/generator/templates/auth_browser.go.tmplM internal/generator/templates/client.go.tmplM internal/generator/templates/doctor.go.tmplM internal/generator/templates/readme.md.tmplM internal/generator/templates/skill.md.tmplM internal/spec/spec.goM internal/spec/spec_test.goM skills/printing-press/references/spec-format.mdM 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 8721df4bf310cac7b0619747af9db366508583c3
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 8 11:58:39 2026 -0700
fix(cli): preserve browser-sniffed request defaults (#737)
---
internal/catalog/catalog.go | 3 +-
internal/cli/generate_test.go | 29 +++++++-
internal/cli/root.go | 10 +--
internal/generator/generator_test.go | 80 ++++++++++++++++++++++
internal/generator/templates/auth_browser.go.tmpl | 4 +-
internal/generator/templates/client.go.tmpl | 13 +++-
internal/generator/templates/doctor.go.tmpl | 10 ++-
internal/generator/templates/readme.md.tmpl | 7 +-
internal/generator/templates/skill.md.tmpl | 7 +-
internal/spec/spec.go | 34 ++++++---
internal/spec/spec_test.go | 13 ++++
skills/printing-press/references/spec-format.md | 6 ++
.../internal/client/client.go | 4 +-
.../internal/client/client.go | 4 +-
.../tier-routing-golden/internal/client/client.go | 4 +-
15 files changed, 205 insertions(+), 23 deletions(-)
diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
index 85494bfa..46aa05e1 100644
--- a/internal/catalog/catalog.go
+++ b/internal/catalog/catalog.go
@@ -64,6 +64,7 @@ var validClientPatterns = map[string]struct{}{
var validHTTPTransports = map[string]struct{}{
"standard": {}, // Plain net/http, default for official APIs
+ "browser-http": {}, // Plain net/http with HTTP/2 disabled for browser-facing websites
"browser-chrome": {}, // Browser-compatible transport for web-discovered/non-official APIs
"browser-chrome-h3": {}, // Chrome-compatible HTTP transport forced through HTTP/3
}
@@ -294,7 +295,7 @@ func (e *Entry) Validate() error {
}
if e.HTTPTransport != "" {
if _, ok := validHTTPTransports[e.HTTPTransport]; !ok {
- return fmt.Errorf("http_transport must be one of: standard, browser-chrome, browser-chrome-h3")
+ return fmt.Errorf("http_transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3")
}
}
if err := validateBearerRefresh(e.BearerRefresh); err != nil {
diff --git a/internal/cli/generate_test.go b/internal/cli/generate_test.go
index 72e6483d..f4067740 100644
--- a/internal/cli/generate_test.go
+++ b/internal/cli/generate_test.go
@@ -471,12 +471,39 @@ func TestMergeSpecsPrefersReplayableBrowserTransportOverUnshippablePageContext(t
mergedRuntimeLast := mergeSpecs([]*spec.APISpec{chromeSpec, runtimeSpec}, "merged")
assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedRuntimeLast.HTTPTransport)
+
+ httpSpec := &spec.APISpec{
+ Name: "http",
+ Version: "0.1.0",
+ BaseURL: "https://http.example.com",
+ HTTPTransport: spec.HTTPTransportBrowserHTTP,
+ Resources: map[string]spec.Resource{},
+ Types: map[string]spec.TypeDef{},
+ }
+ standardSpec := &spec.APISpec{
+ Name: "standard",
+ Version: "0.1.0",
+ BaseURL: "https://standard.example.com",
+ HTTPTransport: spec.HTTPTransportStandard,
+ Resources: map[string]spec.Resource{},
+ Types: map[string]spec.TypeDef{},
+ }
+
+ mergedHTTP := mergeSpecs([]*spec.APISpec{standardSpec, httpSpec}, "merged")
+ assert.Equal(t, spec.HTTPTransportBrowserHTTP, mergedHTTP.HTTPTransport)
+
+ mergedChrome := mergeSpecs([]*spec.APISpec{httpSpec, chromeSpec}, "merged")
+ assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedChrome.HTTPTransport)
}
func TestNormalizeHTTPTransportAllowsBrowserChromeH3(t *testing.T) {
t.Parallel()
- got, err := normalizeHTTPTransport(spec.HTTPTransportBrowserChromeH3)
+ got, err := normalizeHTTPTransport(spec.HTTPTransportBrowserHTTP)
+ require.NoError(t, err)
+ assert.Equal(t, spec.HTTPTransportBrowserHTTP, got)
+
+ got, err = normalizeHTTPTransport(spec.HTTPTransportBrowserChromeH3)
require.NoError(t, err)
assert.Equal(t, spec.HTTPTransportBrowserChromeH3, got)
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 04e88dd9..7eff875d 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -383,7 +383,7 @@ func newGenerateCmd() *cobra.Command {
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Parse spec and show what would be generated without writing files (remote specs are still fetched)")
cmd.Flags().StringVar(&specSource, "spec-source", "", "Spec provenance: official, community, sniffed/browser-sniffed, docs (affects generated client defaults like rate limiting)")
cmd.Flags().StringVar(&clientPattern, "client-pattern", "", "HTTP client pattern: rest (default), proxy-envelope (wraps requests in POST envelope)")
- cmd.Flags().StringVar(&httpTransport, "transport", "", "HTTP transport: standard, browser-chrome, or browser-chrome-h3 (defaults based on spec provenance and reachability)")
+ cmd.Flags().StringVar(&httpTransport, "transport", "", "HTTP transport: standard, browser-http, browser-chrome, or browser-chrome-h3 (defaults based on spec provenance and reachability)")
cmd.Flags().StringVar(&researchDir, "research-dir", "", "Pipeline directory containing research.json and discovery/ for README source credits")
cmd.Flags().IntVar(&maxResources, "max-resources", 0, "Maximum resource groups to generate (default 500, raise for enormous APIs)")
cmd.Flags().IntVar(&maxEndpointsPerResource, "max-endpoints-per-resource", 0, "Maximum endpoints per resource (default 50, raise for large APIs)")
@@ -505,10 +505,10 @@ func normalizeClientPattern(value string) (string, error) {
func normalizeHTTPTransport(value string) (string, error) {
switch value {
- case "", spec.HTTPTransportStandard, spec.HTTPTransportBrowserChrome, spec.HTTPTransportBrowserChromeH3:
+ case "", spec.HTTPTransportStandard, spec.HTTPTransportBrowserHTTP, spec.HTTPTransportBrowserChrome, spec.HTTPTransportBrowserChromeH3:
return value, nil
default:
- return "", fmt.Errorf("--transport must be one of: standard, browser-chrome, browser-chrome-h3 (got %q)", value)
+ return "", fmt.Errorf("--transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3 (got %q)", value)
}
}
@@ -751,8 +751,10 @@ func strongerHTTPTransport(current, candidate string) string {
func httpTransportPriority(value string) int {
switch value {
case spec.HTTPTransportBrowserChromeH3:
- return 3
+ return 4
case spec.HTTPTransportBrowserChrome:
+ return 3
+ case spec.HTTPTransportBrowserHTTP:
return 2
case spec.HTTPTransportStandard:
return 1
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 4a187684..16d4ba34 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1295,6 +1295,47 @@ func TestGenerateBrowserChromeH3Transport(t *testing.T) {
runGoCommand(t, outputDir, "test", "./internal/client")
}
+func TestGenerateBrowserHTTPTransportDisablesHTTP2(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "websurfacehttp",
+ Version: "0.1.0",
+ BaseURL: "https://www.example.com",
+ HTTPTransport: spec.HTTPTransportBrowserHTTP,
+ Auth: spec.AuthConfig{Type: "none"},
+ Config: spec.ConfigSpec{
+ Format: "toml",
+ Path: "~/.config/websurfacehttp-pp-cli/config.toml",
+ },
+ Resources: map[string]spec.Resource{
+ "posts": {
+ Description: "Browse posts",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/",
+ Description: "List posts",
+ },
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "websurfacehttp-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ clientGo := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+ assert.Contains(t, clientGo, `"crypto/tls"`)
+ assert.Contains(t, clientGo, `transport := http.DefaultTransport.(*http.Transport).Clone()`)
+ assert.Contains(t, clientGo, `transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)`)
+ assert.NotContains(t, clientGo, `"github.com/enetx/surf"`)
+ assert.NotContains(t, clientGo, `Impersonate()`)
+
+ gomod := readGeneratedFile(t, outputDir, "go.mod")
+ assert.NotContains(t, gomod, "github.com/enetx/surf")
+}
+
func TestGenerateHTMLExtractionEndpoint(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -4333,6 +4374,22 @@ func TestGeneratedDoctor_AuthVerifyPathProbesEndpoint(t *testing.T) {
assert.NotContains(t, content, "inconclusive (HTTP %d from base URL")
}
+func TestGeneratedDoctor_HealthCheckPathProbesEndpoint(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("healthdoc")
+ apiSpec.HealthCheckPath = "api/marketStatus"
+
+ outputDir := filepath.Join(t.TempDir(), "healthdoc-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ doctorSrc := readGeneratedFile(t, outputDir, "internal", "cli", "doctor.go")
+ assert.Contains(t, doctorSrc, `healthPath := "api/marketStatus"`)
+ assert.Contains(t, doctorSrc, `if !strings.HasPrefix(healthPath, "/") {`)
+ assert.Contains(t, doctorSrc, `reachBody, reachErr := c.Get(healthPath, nil)`)
+ assert.NotContains(t, doctorSrc, `reachBody, reachErr := c.Get("/", nil)`)
+}
+
func TestGeneratedDoctor_InterstitialMarkersAreTitleAnchored(t *testing.T) {
t.Parallel()
@@ -4639,6 +4696,29 @@ func TestGenerate_UserAgentOverrideGatedByBrowserTransport(t *testing.T) {
assert.True(t, os.IsNotExist(err), "auth.go should not be emitted for auth.type:none specs")
}
+func TestGenerateRequiredUserAgentHeaderBeatsDefaultUserAgent(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("browserheaders")
+ apiSpec.RequiredHeaders = []spec.RequiredHeader{
+ {Name: "User-Agent", Value: "Mozilla/5.0 Browser Sniff"},
+ {Name: "Referer", Value: "https://www.example.com/"},
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "browserheaders-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ clientSrc := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+ require.Contains(t, clientSrc, `req.Header.Set("User-Agent", "Mozilla/5.0 Browser Sniff")`)
+ require.Contains(t, clientSrc, `req.Header.Set("Referer", "https://www.example.com/")`)
+ assert.Contains(t, clientSrc, `if req.Header.Get("User-Agent") == "" {`)
+ assert.Contains(t, clientSrc, `req.Header.Set("User-Agent", "browserheaders-pp-cli/0.1.0")`)
+
+ doctorSrc := readGeneratedFile(t, outputDir, "internal", "cli", "doctor.go")
+ require.Contains(t, doctorSrc, `authHeaders["User-Agent"] = "Mozilla/5.0 Browser Sniff"`)
+ assert.NotContains(t, doctorSrc, `authHeaders["User-Agent"] = "browserheaders-pp-cli"`)
+}
+
func TestGenerateObjectBodyDefaultsAreParsedAsJSON(t *testing.T) {
t.Parallel()
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index bfbebef9..a23b4f42 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -1282,7 +1282,9 @@ func validateComposedAuth(authHeader string) error {
req.Header.Set("{{.Name}}", "{{.Value}}")
{{- end}}
{{- if not .UsesBrowserManagedUserAgent}}
- req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+ if req.Header.Get("User-Agent") == "" {
+ req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+ }
{{- end}}
client := &http.Client{Timeout: 5 * time.Second}
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index f3b58cdd..dc2fd59b 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -5,6 +5,9 @@ package client
import (
"bytes"
+{{- if .UsesHTTP2DisabledTransport}}
+ "crypto/tls"
+{{- end}}
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -259,7 +262,11 @@ func (e *APIError) Error() string {
}
func newHTTPClient(timeout time.Duration, jar http.CookieJar) *http.Client {
-{{- if .UsesBrowserHTTPTransport}}
+{{- if .UsesHTTP2DisabledTransport}}
+ transport := http.DefaultTransport.(*http.Transport).Clone()
+ transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
+ return &http.Client{Timeout: timeout, Jar: jar, Transport: transport}
+{{- else if .UsesBrowserHTTPTransport}}
builder := surf.NewClient().
Builder().
Impersonate().
@@ -668,7 +675,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
req.Header.Set(k, v)
}
{{- if not .UsesBrowserManagedUserAgent}}
- req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+ if req.Header.Get("User-Agent") == "" {
+ req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+ }
{{- end}}
resp, err := c.HTTPClient.Do(req)
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index ebcb9a87..9d403d58 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -333,7 +333,15 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["api"] = fmt.Sprintf("client init error: %s", clientErr)
} else {
// Step 1: Basic reachability via the configured transport.
+{{- if .HealthCheckPath}}
+ healthPath := "{{.HealthCheckPath}}"
+ if !strings.HasPrefix(healthPath, "/") {
+ healthPath = "/" + healthPath
+ }
+ reachBody, reachErr := c.Get(healthPath, nil)
+{{- else}}
reachBody, reachErr := c.Get("/", nil)
+{{- end}}
var reachAPIErr *client.APIError
switch {
case reachErr == nil:
@@ -396,7 +404,7 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
{{- range .RequiredHeaders}}
authHeaders["{{.Name}}"] = "{{.Value}}"
{{- end}}
-{{- if not .UsesBrowserManagedUserAgent}}
+{{- if and (not .UsesBrowserManagedUserAgent) (not (.HasRequiredHeader "User-Agent"))}}
authHeaders["User-Agent"] = "{{.Name}}-pp-cli"
{{- end}}
_, authErr := c.GetWithHeaders(verifyPath, authParams, authHeaders)
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index be54695c..b246711f 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -515,7 +515,12 @@ Environment variables:
- **{{.Symptom}}** — {{.Fix}}
{{- end}}
{{- end}}
-{{- if .UsesBrowserHTTPTransport}}
+{{- if .UsesHTTP2DisabledTransport}}
+
+## HTTP Transport
+
+This CLI uses standard HTTP transport with HTTP/2 disabled for browser-facing endpoints. It does not require a resident browser process for normal API calls.
+{{- else if .UsesBrowserHTTPTransport}}
## HTTP Transport
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 3032ed40..92d52654 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -95,7 +95,12 @@ These capabilities aren't available in any other tool for this API.
{{- end}}
{{- end}}
{{- end}}
-{{- if .UsesBrowserHTTPTransport}}
+{{- if .UsesHTTP2DisabledTransport}}
+
+## HTTP Transport
+
+This CLI uses standard HTTP transport with HTTP/2 disabled for browser-facing endpoints. It does not require a resident browser process for normal API calls.
+{{- else if .UsesBrowserHTTPTransport}}
## HTTP Transport
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 9dd81554..2b4776d1 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -25,6 +25,7 @@ const (
const (
HTTPTransportStandard = "standard" // default for official API clients
+ HTTPTransportBrowserHTTP = "browser-http" // stdlib transport with HTTP/2 disabled for browser-facing web surfaces
HTTPTransportBrowserChrome = "browser-chrome" // Chrome-impersonated transport for browser-facing web surfaces
HTTPTransportBrowserChromeH3 = "browser-chrome-h3" // Chrome-impersonated transport forced through HTTP/3 for stricter bot screens
)
@@ -109,11 +110,12 @@ type APISpec struct {
Kind string `yaml:"kind,omitempty" json:"kind,omitempty"` // "rest" (default) or "synthetic" — synthetic CLIs aggregate multiple sources beyond the spec; dogfood's path-validity check is relaxed accordingly
SpecSource string `yaml:"spec_source,omitempty" json:"spec_source,omitempty"` // official, community, sniffed, docs — affects generated client defaults
ClientPattern string `yaml:"client_pattern,omitempty" json:"client_pattern,omitempty"` // rest (default), proxy-envelope — affects generated HTTP client
- HTTPTransport string `yaml:"http_transport,omitempty" json:"http_transport,omitempty"` // standard (default for official APIs), browser-chrome, or browser-chrome-h3
- ProxyRoutes map[string]string `yaml:"proxy_routes,omitempty" json:"proxy_routes,omitempty"` // path prefix → service name for proxy-envelope routing
- BearerRefresh BearerRefreshConfig `yaml:"bearer_refresh,omitempty" json:"bearer_refresh,omitzero"` // live-source metadata for rotating public client bearer tokens
- WebsiteURL string `yaml:"website_url,omitempty" json:"website_url,omitempty"` // product/company website (not the API base URL)
- Category string `yaml:"category,omitempty" json:"category,omitempty"` // catalog category (e.g., productivity, developer-tools) — used for library install path
+ HTTPTransport string `yaml:"http_transport,omitempty" json:"http_transport,omitempty"` // standard (default for official APIs), browser-http, browser-chrome, or browser-chrome-h3
+ HealthCheckPath string `yaml:"health_check_path,omitempty" json:"health_check_path,omitempty"`
+ ProxyRoutes map[string]string `yaml:"proxy_routes,omitempty" json:"proxy_routes,omitempty"` // path prefix → service name for proxy-envelope routing
+ BearerRefresh BearerRefreshConfig `yaml:"bearer_refresh,omitempty" json:"bearer_refresh,omitzero"` // live-source metadata for rotating public client bearer tokens
+ WebsiteURL string `yaml:"website_url,omitempty" json:"website_url,omitempty"` // product/company website (not the API base URL)
+ Category string `yaml:"category,omitempty" json:"category,omitempty"` // catalog category (e.g., productivity, developer-tools) — used for library install path
Auth AuthConfig `yaml:"auth" json:"auth"`
TierRouting TierRoutingConfig `yaml:"tier_routing,omitempty" json:"tier_routing,omitzero"`
RequiredHeaders []RequiredHeader `yaml:"required_headers,omitempty" json:"required_headers,omitempty"`
@@ -290,7 +292,7 @@ func (s *APISpec) EffectiveHTTPTransport() string {
return HTTPTransportStandard
}
switch s.HTTPTransport {
- case HTTPTransportStandard, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
+ case HTTPTransportStandard, HTTPTransportBrowserHTTP, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
return s.HTTPTransport
}
switch s.SpecSource {
@@ -314,6 +316,10 @@ func (s *APISpec) UsesBrowserHTTP3Transport() bool {
return s.EffectiveHTTPTransport() == HTTPTransportBrowserChromeH3
}
+func (s *APISpec) UsesHTTP2DisabledTransport() bool {
+ return s.EffectiveHTTPTransport() == HTTPTransportBrowserHTTP
+}
+
func (s *APISpec) UsesBrowserManagedUserAgent() bool {
switch s.EffectiveHTTPTransport() {
case HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
@@ -323,6 +329,18 @@ func (s *APISpec) UsesBrowserManagedUserAgent() bool {
}
}
+func (s *APISpec) HasRequiredHeader(name string) bool {
+ if s == nil {
+ return false
+ }
+ for _, header := range s.RequiredHeaders {
+ if strings.EqualFold(strings.TrimSpace(header.Name), name) {
+ return true
+ }
+ }
+ return false
+}
+
func (s *APISpec) HasHTMLExtraction() bool {
if s == nil {
return false
@@ -1514,9 +1532,9 @@ func (s *APISpec) Validate() error {
return fmt.Errorf("at least one resource is required")
}
switch s.HTTPTransport {
- case "", HTTPTransportStandard, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
+ case "", HTTPTransportStandard, HTTPTransportBrowserHTTP, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
default:
- return fmt.Errorf("http_transport must be one of: standard, browser-chrome, browser-chrome-h3")
+ return fmt.Errorf("http_transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3")
}
if err := validateExtraCommands(s.ExtraCommands); err != nil {
return err
diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go
index 8ce18939..0a724dca 100644
--- a/internal/spec/spec_test.go
+++ b/internal/spec/spec_test.go
@@ -2454,6 +2454,14 @@ func TestHTTPTransportValidationAndDefaults(t *testing.T) {
assert.Equal(t, HTTPTransportBrowserChrome, sniffed.EffectiveHTTPTransport())
require.NoError(t, sniffed.Validate())
+ browserHTTP := base
+ browserHTTP.HTTPTransport = HTTPTransportBrowserHTTP
+ assert.Equal(t, HTTPTransportBrowserHTTP, browserHTTP.EffectiveHTTPTransport())
+ assert.False(t, browserHTTP.UsesBrowserHTTPTransport())
+ assert.True(t, browserHTTP.UsesHTTP2DisabledTransport())
+ assert.False(t, browserHTTP.UsesBrowserManagedUserAgent())
+ require.NoError(t, browserHTTP.Validate())
+
community := base
community.SpecSource = "community"
assert.Equal(t, HTTPTransportBrowserChrome, community.EffectiveHTTPTransport())
@@ -2477,6 +2485,11 @@ func TestHTTPTransportValidationAndDefaults(t *testing.T) {
assert.False(t, runtime.UsesBrowserManagedUserAgent())
require.ErrorContains(t, runtime.Validate(), "http_transport must be one of")
+ requiredUA := base
+ requiredUA.RequiredHeaders = []RequiredHeader{{Name: "user-agent", Value: "Mozilla/5.0"}}
+ assert.True(t, requiredUA.HasRequiredHeader("User-Agent"))
+ assert.False(t, requiredUA.HasRequiredHeader("Referer"))
+
invalid := base
invalid.HTTPTransport = "lynx"
require.ErrorContains(t, invalid.Validate(), "http_transport must be one of")
diff --git a/skills/printing-press/references/spec-format.md b/skills/printing-press/references/spec-format.md
index 2ddd4f48..5425986a 100644
--- a/skills/printing-press/references/spec-format.md
+++ b/skills/printing-press/references/spec-format.md
@@ -10,6 +10,11 @@ name: my-api # string (REQUIRED) CLI binary prefix, e.g. "my
description: "My API CLI" # string shown in command help
version: "0.1.0" # string baked into generated binary
base_url: "https://api.example.com/v1" # string (REQUIRED) default base API URL
+http_transport: standard # string optional: standard | browser-http | browser-chrome | browser-chrome-h3
+health_check_path: "/health" # string optional doctor reachability path; defaults to /
+required_headers: # []RequiredHeader optional headers sent on every request
+ - name: User-Agent
+ value: "Mozilla/5.0 ..."
auth: # object (AuthConfig)
type: api_key # string: api_key | oauth2 | bearer_token | none
@@ -79,6 +84,7 @@ name: stytch # CLI binary prefix => stytch-cli
description: "Stytch authentication API CLI" # Root help text and README summary
version: "0.1.0" # Printed by `stytch-cli version`
base_url: "https://api.stytch.com/v1" # Default base URL endpoint paths are joined against
+health_check_path: "/sessions" # Optional doctor reachability path; omit to probe /
auth:
type: api_key # Uses API key style auth
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 f3297a7f..6966ce01 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
@@ -231,7 +231,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
- req.Header.Set("User-Agent", "printing-press-oauth2-pp-cli/1.0.0")
+ if req.Header.Get("User-Agent") == "" {
+ req.Header.Set("User-Agent", "printing-press-oauth2-pp-cli/1.0.0")
+ }
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 b9cddae2..52c5cced 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
@@ -226,7 +226,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
- req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
+ if req.Header.Get("User-Agent") == "" {
+ req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
+ }
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 396a5931..4f13cb33 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
@@ -329,7 +329,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
- req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
+ if req.Header.Get("User-Agent") == "" {
+ req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
+ }
resp, err := c.HTTPClient.Do(req)
if err != nil {
← 1d26ae77 fix(cli): guard generated CLI build safety (#736)
·
back to Cli Printing Press
·
fix(cli): hydrate promote state across scopes (#738) dfdf1831 →