Skip to content

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

Merged
2ynn merged 6 commits into
speakeasy-api:mainfrom
LukasParke:fix/python-empty-credentials
Oct 26, 2026
Merged

2ynn merged 6 commits into
speakeasy-api:mainfrom
LukasParke:fix/python-empty-credentials

Conversation

@LukasParke

@LukasParke LukasParke commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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. Changes:
    • test_global_security_fields_ordering is parametrized. The populated case is the original test and now also checks that the api_key_auth value is sent. The empty-string case passes api_key_auth="" with basic_http set: api_key_auth stays selected, no Authorization header is sent, and the endpoint returns 401.
    • test_global_security_empty_ignores_env_vars (auth-global-security-empty): 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 (auth-global-security-flattening-empty; 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.
  • New test IDs auth-global-security-empty and auth-global-security-flattening-empty are registered in internal/features/tests.go (go generate ./internal/features/...). Every other target's isTestSkipped lists them as skipped. go run ./tests -lang pythonv2 reports both IDs as covered, with no unregistered IDs.
  • 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.

@github-actions github-actions Bot added the python Trigger (106) snapshot tests for python label Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

…nt fallback applies

A security value passed as an empty string (for example
`SDK(api_key_auth="")`) was treated as provided: `get_security_from_env`
returned early, so the `envVarPrefix` fallback never ran, and
`get_security` serialized the empty value, producing headers such as
`Authorization: Bearer ` that httpx rejects as an illegal header value.

Empty-string credentials are now skipped when building request
security, so a later non-empty scheme can be selected, and a global
security object whose credentials are all empty falls back to the
environment. Basic and custom schemes keep their existing behaviour,
so an explicitly empty username or password is still sent.
@LukasParke
LukasParke force-pushed the fix/python-empty-credentials branch from d6909a8 to ef172ec Compare October 21, 2026 19:13
@LukasParke
LukasParke marked this pull request as ready for review October 21, 2026 19:19
@LukasParke
LukasParke requested a review from a team as a code owner October 21, 2026 19:19
@LukasParke
LukasParke requested a review from mfbx9da4 October 21, 2026 19:19
@LukasParke

Copy link
Copy Markdown
Contributor Author

recheck

1 similar comment
@LukasParke

Copy link
Copy Markdown
Contributor Author

recheck

@2ynn

2ynn commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@LukasParke I don't agree with the premise of this PR. Our SDKs intentionally make a distinction between an omitted credential (e.g. SDK()) and an explicitly empty string (e.g. SDK(api_key_auth=""))

For example, a user wanting to disable security may explicitly set it empty:

sdk = SDK(security=Security())

Following your PR, environment credentials would get picked up in this case, potentially authenticating a previously unauthenticated request.

… environment

An explicitly empty credential, such as SDK(api_key_auth="") or
SDK(security=Security()), now stays unauthenticated even when
envVarPrefix environment variables are set. get_security_from_env is
back to its previous behaviour and only consults the environment when
no security was passed.

Empty-string credential values are still skipped when serializing, so
no "Authorization: Bearer " header or empty API key header or query
parameter is sent. HTTP Basic and custom schemes are unchanged.
@LukasParke LukasParke changed the title fix(python): treat empty-string credentials as unset so the environment fallback applies fix(python): don't send empty-string security credentials Oct 21, 2026
@LukasParke

Copy link
Copy Markdown
Contributor Author

@2ynn Thanks, that's a good point and I agree. An explicitly empty credential should mean "no credential", not "look in the environment".

I've removed the environment fallback in 90c32e0. get_security_from_env is back to its current behaviour, so SDK(security=Security()) and SDK(api_key_auth="") stay unauthenticated even when the envVarPrefix variables are set. There are tests for both cases with the env var set.

What's left only stops serializing empty values. Today "" on a bearer/OAuth2 scheme yields Authorization: Bearer , which httpx rejects as an illegal header value, so the call errors instead of going out unauthenticated. With this change no header (or empty API key header/query parameter) is sent, which matches TypeScript's resolveSecurity/applyBearer. get_security also skips a "" field so that a later non-empty scheme the caller passed in the same Security object is used. That's the same rule TypeScript uses to pick a security option, and it never reads the environment. Basic and custom schemes are unchanged.

@2ynn 2ynn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today "" on a bearer/OAuth2 scheme yields Authorization: Bearer , which httpx rejects as an illegal header value, so the call errors instead of going out unauthenticated. With this change no header (or empty API key header/query parameter) is sent, which matches TypeScript's resolveSecurity/applyBearer.

I agree with that part!

Comment thread templates/templates/pythonv2/tests/primary/test_auth_additional.py Outdated
Scheme selection in get_security again matches the declared security
order: a field set to "" stays selected like any other non-None value
instead of being skipped in favour of a later scheme. The empty value is
still not serialized, so no credential is sent.

Replace the selection-skipping test with one asserting that an empty
first scheme stays selected and no Authorization header is sent.
Comment thread templates/templates/pythonv2/tests/primary/test_auth_additional.py Outdated
Comment thread templates/templates/pythonv2/tests/primary/test_auth_additional.py Outdated
LukasParke and others added 2 commits October 26, 2026 19:23
…empty-credential test IDs

Fold the explicit-empty-string case into test_global_security_fields_ordering
instead of a separate test, and assert which Authorization header is sent.

Register auth-global-security-empty and auth-global-security-flattening-empty
for the empty-credential tests, which previously reused unrelated IDs. Other
targets skip both IDs.
@2ynn
2ynn added this pull request to the merge queue Oct 26, 2026
Merged via the queue into speakeasy-api:main with commit 50f2665 Oct 26, 2026
450 of 453 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

python Trigger (106) snapshot tests for python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants