feat(internal): skip Authorization header when bearer token is empty string#16381
feat(internal): skip Authorization header when bearer token is empty string#16381devin-ai-integration[bot] wants to merge 7 commits into
Conversation
…y string When api_key is set to an empty string, the generated SDKs now skip sending the Authorization header entirely. This allows using the SDK against backends that do not require authentication without needing to set a dummy token value. Changes: - TypeScript: BearerToken.toAuthorizationHeader returns undefined for empty string; BearerAuthProviderGenerator returns empty headers for empty token - Python: sync and async get_headers check truthiness (not just None) - Java: BearerAuthProviderGenerator, RequestOptionsGenerator, and AbstractRootClientGenerator check !token.isEmpty() - Go: already handled (checks != "" before setting header) Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
| message: ${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE, | ||
| }); | ||
| } | ||
| if (${tokenVar} === "") { |
There was a problem hiding this comment.
Strict-mode TypeScript SDK silently bypasses auth for empty-string bearer token; canCreate inconsistency blocks multi-auth fallback
In the neverThrowErrors=false (strict) branch (lines 291–305), the generated SDK throws for null but silently returns { headers: {} } — no Authorization header — for "". Users who opted into strict error mode to catch misconfigured credentials will instead get unauthenticated requests without any signal when the token resolves to an empty string.
This compounds with canCreate (line 231), which uses options?.[TOKEN_PARAM] != null — so canCreate("") returns true. In multi-auth-scheme APIs the runtime selects the first provider whose canCreate returns true; bearer is selected for "", emits no auth headers, and other providers are never tried.
Concrete trigger: process.env.MY_API_KEY set to "" (an explicitly-blanked environment variable) passes canCreate, skips the auth header, and the request is sent unauthenticated. If the env var has a tokenEnvVar fallback, the generated code is (supplierGet) ?? process.env?.[ENV_TOKEN]; a "" value in the env short-circuits the nullish-coalescing and flows into the empty-string bypass.
The neverThrowErrors=true branch is unambiguous — it's a soft-mode that accepts missing or empty tokens. The strict branch should either throw for "" (like it does for null) or at minimum ensure canCreate rejects "" so the multi-auth fallback chain is not broken.
Prompt To Fix With AI
Two changes are needed:
1. In `generateGetAuthRequestStatements`, in the `neverThrowErrors=false` branch, treat `""` the same as `null` (throw an error) instead of silently returning empty headers:
```typescript
// Replace lines 298-300:
if (${tokenVar} === "") {
return { headers: {} };
}
```
with:
```typescript
if (${tokenVar} === "") {
throw new ${errorConstructor}({
message: ${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE,
});
}
```
2. In `generatecanCreateStatements` (line 231), add an empty-string exclusion so `canCreate("")` returns `false` and multi-auth fallback works correctly:
```typescript
// Replace:
return `return options?.${wrapperAccess}[TOKEN_PARAM] != null${envCheck};`;
// With:
return `return options?.${wrapperAccess}[TOKEN_PARAM] != null && options?.${wrapperAccess}[TOKEN_PARAM] !== ""${envCheck};`;
```
If intentional no-auth via `""` must be supported in strict mode, document it clearly and at minimum fix the `canCreate` inconsistency so multi-auth fallback is not silently broken.Severity: medium | Confidence: 82% | React with 👍 if useful or 👎 if not
There was a problem hiding this comment.
This behavior is intentional. The feature request is specifically: when a user sets api_key="", they are explicitly opting out of auth — not misconfiguring it. This is for backends that don't require authentication where the SDK currently forces a dummy value.
Strict mode (neverThrowErrors=false): null/undefined means "forgot to set a token" → error. "" means "I explicitly don't want auth" → skip the header. These are semantically different — an empty string is a deliberate value, not an omission.
canCreate(""): Returning true is correct here. The user IS providing a value (empty string as an opt-out). In multi-auth scenarios, if a user explicitly sets the bearer token to "", they want no auth — not a fallback to a different auth method. The bearer provider should be selected (because the user configured it) and should emit no headers (because that's what "" signals).
Environment variable trigger: process.env.MY_API_KEY="" is exactly the use case — a deployment where the backend doesn't need auth blanks the env var, and the SDK should just not send the Authorization header.
| writer.write_line(f"if {param.constructor_parameter_name}:") | ||
| with writer.indent(): | ||
| writer.write_line( | ||
| f'headers["{param.header_key}"] = f"{param.header_prefix} {{{param.constructor_parameter_name}}}"' | ||
| ) | ||
| else: | ||
| writer.write_line( | ||
| f'headers["{param.header_key}"] = f"{param.header_prefix} {{self.{param.getter_method.name}()}}"' | ||
| f"{param.constructor_parameter_name} = self.{param.getter_method.name}()" | ||
| ) | ||
| writer.write_line(f"if {param.constructor_parameter_name}:") | ||
| with writer.indent(): | ||
| writer.write_line( | ||
| f'headers["{param.header_key}"] = f"{param.header_prefix} {{{param.constructor_parameter_name}}}"' | ||
| ) | ||
| elif param.private_member_name is not None: | ||
| if param.type_hint.is_optional: | ||
| writer.write_line(f"if self.{param.private_member_name} is not None:") | ||
| writer.write_line(f"if self.{param.private_member_name}:") | ||
| writer.indent() | ||
| else: | ||
| writer.write_line(f"if self.{param.private_member_name}:") | ||
| writer.indent() | ||
| writer.write_line( | ||
| f'headers["{param.header_key}"] = f"{param.header_prefix} {{self.{param.private_member_name}}}"' | ||
| ) | ||
| if param.type_hint.is_optional: | ||
| writer.outdent() | ||
| writer.outdent() |
There was a problem hiding this comment.
🚩 Python truthiness check scope is broader than bearer auth — affects all prefixed header auth schemes
The PR description says this is about bearer tokens, but the Python code changes at lines 630, 639, 646, and 649 apply truthiness checks (if value: instead of if value is not None:) to ALL auth headers with a header_prefix — not just bearer tokens. This includes HeaderAuthScheme parameters (e.g., X-API-Key: Token <value>) configured at client_wrapper_generator.py:834. For non-optional header auth schemes with a prefix (lines 648-654), the old code unconditionally set the header; the new code wraps it in a truthiness check. This means if a user sets an API key to empty string, the header is now silently omitted. This appears to be intentionally consistent behavior, but the PR description doesn't mention it. Reviewers should confirm this broader scope is desired.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch on the broader scope. This is intentional — the feature request is to skip the auth header when the token value is an empty string, and this should be consistent across all auth schemes (bearer, API key with prefix, etc.), not just bearer tokens specifically. If a user explicitly sets any auth token to "", the intent is "don't send auth" regardless of the auth scheme type.
I'll update the PR description to make this broader scope explicit.
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
Test ResultsAll tests passed — verified the generated SDK behavior across TypeScript, Python, and Java. End-to-end behavior verification (7/7 passed)
TypeScript unit tests (547/547 passed)5 snapshots updated to reflect new CI seed test results (all passed)
|
…-empty-auth-header
Description
When
api_keyis set to""(empty string), the generated SDKs now skip sending theAuthorizationheader entirely. This allows using SDKs against backends that do not require authentication without needing to set a dummy token value (e.g.,api_key="dummy").Scope: This applies to all auth headers with prefixes — not just bearer tokens. Any prefixed auth scheme (bearer, API key with prefix like
X-API-Key: Token <value>, etc.) will skip the header when the token is an empty string. This is intentional:""is treated as an explicit "don't send auth" signal across all auth types.Changes Made
TypeScript:
BearerToken.toAuthorizationHeader()returnsundefinedfor empty string (core utility used by legacy path)BearerAuthProviderGeneratorreturns empty headers{}when resolved token is""(new auth provider path)neverThrowErrors=false):null/undefined→ throws error (missing token),""→ returns empty headers (intentional opt-out)canCreate("")returnstrueby design — the user is providing a value (empty string as opt-out), not omitting configurationPython:
get_headers: truthiness check for all prefixed auth params — coversNone,"", and0-length tokens consistently across all auth scheme types (bearer, API key with prefix, etc.)Java:
BearerAuthProviderGenerator.getAuthHeaders(): wrapsheaders.put(...)inif (!token.isEmpty())RequestOptionsGenerator: adds!isEmpty()to the null check for bearer token fieldAbstractRootClientGenerator: adds!isEmpty()to the null check inconfigureAuthMethodGo: Already correct —
ToHeader()checksif r.Token != ""before setting the header.Testing
Link to Devin session: https://app.devin.ai/sessions/2099af505c4b4047bde5e956a458b333