Skip to content

feat(internal): skip Authorization header when bearer token is empty string#16381

Open
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1781011046-skip-empty-auth-header
Open

feat(internal): skip Authorization header when bearer token is empty string#16381
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1781011046-skip-empty-auth-header

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Description

When api_key is set to "" (empty string), the generated SDKs now skip sending the Authorization header 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() returns undefined for empty string (core utility used by legacy path)
  • BearerAuthProviderGenerator returns empty headers {} when resolved token is "" (new auth provider path)
  • In strict mode (neverThrowErrors=false): null/undefined → throws error (missing token), "" → returns empty headers (intentional opt-out)
  • canCreate("") returns true by design — the user is providing a value (empty string as opt-out), not omitting configuration

Python:

  • Sync/async get_headers: truthiness check for all prefixed auth params — covers None, "", and 0-length tokens consistently across all auth scheme types (bearer, API key with prefix, etc.)

Java:

  • BearerAuthProviderGenerator.getAuthHeaders(): wraps headers.put(...) in if (!token.isEmpty())
  • RequestOptionsGenerator: adds !isEmpty() to the null check for bearer token field
  • AbstractRootClientGenerator: adds !isEmpty() to the null check in configureAuthMethod

Go: Already correct — ToHeader() checks if r.Token != "" before setting the header.

  • Updated README.md generator (if applicable)

Testing

  • TypeScript unit tests (547 pass, 5 snapshots updated)
  • Seed test outputs updated: ts-sdk (170 files), python-sdk (191 files), java-sdk (142 files)
  • All seed-test-results CI checks pass (ts-sdk, python-sdk, java-sdk, go-sdk, etc.)
  • Java Spotless formatting verified
  • Manual end-to-end verification across TS, Python, Java (7/7 behavior tests passed)

Link to Devin session: https://app.devin.ai/sessions/2099af505c4b4047bde5e956a458b333


Open in Devin Review

…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-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title feat(generators): skip Authorization header when bearer token is empty string feat(internal): skip Authorization header when bearer token is empty string Jun 9, 2026
Co-Authored-By: will.kendall@buildwithfern.com <wpk235@gmail.com>
message: ${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE,
});
}
if (${tokenVar} === "") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +630 to +654
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()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-06-18T05:34:02Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
java-sdk square 218s (n=5) 297s (n=5) 250s +32s (+14.7%)
python-sdk square 138s (n=5) 244s (n=5) 140s +2s (+1.4%)
ts-sdk square 235s (n=5) 239s (n=5) 235s +0s (+0.0%)

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 fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-06-18T05:34:02Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-06-18 15:57 UTC

devin-ai-integration Bot and others added 3 commits June 9, 2026 14:30
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>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Test Results

All tests passed — verified the generated SDK behavior across TypeScript, Python, and Java.

End-to-end behavior verification (7/7 passed)
# Test Result
1 TS: Empty string token → { headers: {} } (no Authorization)
2 TS: Valid token → Authorization: Bearer my-secret-token
3 TS: Null token → throws error with correct message
4 TS: BearerToken.toAuthorizationHeader("")undefined
5 Python: api_key="" → no Authorization header in get_headers()
6 Python: api_key="my-secret-token" → correct Authorization header
7 Java: RequestOptions.getHeaders() has !isEmpty() guard
TypeScript unit tests (547/547 passed)
Test Files  26 passed (26)
     Tests  547 passed (547)
  Duration  32.43s

5 snapshots updated to reflect new BearerAuthProvider behavior.

CI seed test results (all passed)
  • seed-test-results (ts-sdk): ✅
  • seed-test-results (python-sdk): ✅
  • seed-test-results (java-sdk): ✅
  • seed-test-results (go-sdk): ✅
  • Plus csharp, ruby, php, rust, swift, etc.

Devin session

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants