fix(python): don't send empty-string security credentials

LU
4h ago
+90-1

Why

In a generated Python SDK, a security credential passed as an empty string (for example SDK(api_key_auth="") or Security(access_token="")) was serialized as-is. For bearer, OAuth2 and OpenID Connect schemes this produced Authorization: Bearer , which httpx rejects as an illegal header value (LocalProtocolError). For API keys it sent an empty header or query parameter.

What changed

_parse_security_scheme_value in templates/templates/pythonv2/auxiliary/{{getSourcePath}}/utils/security.py.stmpl returns without writing a header or query parameter when the value is "". This covers top-level fields as well as values nested in security options and scheme models.

What stays the same:

  • Scheme selection. get_security still selects the first non-None field in the declared security order, so an explicit "" keeps its scheme selected and the request is sent without a credential. It does not fall back to a later scheme.
  • Environment fallback. An explicit "" does not fall back to the envVarPrefix environment variable.
  • HTTP Basic and custom schemes. These go through _parse_basic_auth_scheme and custom handling, so an empty username or password is still sent as before (the existing auth-basic-auth-empty test covers this).
  • Only exact "" is affected. Whitespace-only strings and non-string values are not changed.

Generated output: zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/utils/security.py (plus its gen.lock checksum) is the only review SDK change. Usage snippets are not touched.

Changeset: globalSecurity / pythonv2, patch.

Testing

  • make check-template-pythonv2, npm run format, git diff --check: pass.
  • TARGET=primary make test-pythonv2: 672 passed. Adds:
    • test_global_security_fields_ordering_keeps_empty_string_selected: Security(access_token="", oauth2="global") keeps the first field selected and sends no Authorization header.
    • test_global_security_empty_ignores_env_vars: with SPEAKEASY_ACCESS_TOKEN set, both Security() and Security(access_token="") send no Authorization header.
  • TARGET=quaternary make test-pythonv2 (flattened global security with envVarPrefix): 248 passed, 1 skipped. Adds test_global_security_flattening_empty_string_ignores_env_var (SDK(api_key_auth="") with SPEAKEASY_API_KEY_AUTH set sends no Authorization header).
  • TARGET=security-options make test-pythonv2: 6 passed, 194 skipped.
  • With the template change removed, the empty-string tests fail because an Authorization header is sent. The omitted case already passes on main. It checks that the environment is not consulted when a security object is passed.
  • TARGET=review make build-pythonv2: the review SDK diff contains only the change above.
  • The new tests record the existing auth-global-security-fields-ordering and auth-global-security-flattening-env-var-fallback test IDs instead of adding new IDs, so other targets' skip lists don't need to change.
  • Not run: other targets (no template changes there) and the full make test matrix.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.claude/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.

Summary by cubic

Fixes generated Python SDKs so empty-string security credentials are never serialized as an empty bearer token, API key header, or query parameter, which httpx rejects.

  • Empty-string credentials are skipped when building request security, letting a later non-empty scheme win.
  • An explicitly empty global security object now stays unauthenticated; the environment fallback only applies when no security is passed at all.
  • Only exact "" counts as unset; whitespace-only values, non-string values, and HTTP Basic/custom schemes keep existing behavior.