Skip to content

fix(rust): apply client custom headers to the OAuth token request - #17296

Open
fern-api[bot] wants to merge 2 commits into
mainfrom
devin/rust-oauth-token-custom-headers
Open

fix(rust): apply client custom headers to the OAuth token request#17296
fern-api[bot] wants to merge 2 commits into
mainfrom
devin/rust-oauth-token-custom-headers

Conversation

@fern-api

@fern-api fern-api Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

The Rust client-credentials token exchange was the only request in the generated SDK that bypassed apply_custom_headers: fetch_oauth_token fired straight off the shared reqwest::Client, while every other request goes through send_requestapply_auth_headers + apply_custom_headers. Any API whose gateway requires a header on its token endpoint therefore 401s on the exchange, and the failure surfaces on the first SDK call rather than at the token request:

Error: UnauthorizedError { message: "missing authorization header, bearer-token cookie or <gateway-header>", auth_type: None }

X-Fern-SDK-* telemetry headers were also missing from token requests for the same reason.

-        let response = self
+        let mut request = self
             .client
             .request(Method::POST, &url)
             .json(&body)
-            .send()
+            .build()
+            .map_err(ApiError::Network)?;
+        self.apply_custom_headers(&mut request, &None)?;
+
+        let response = self
+            .client
+            .execute(request)
             .await
             .map_err(ApiError::Network)?;

Only config-level headers are applied. Request-level additional_headers are deliberately excluded: the token is cached in OAuthTokenProvider and shared across requests, so it must not depend on the options of whichever request happens to trigger the fetch. Auth headers are excluded because this request is what produces the credential they would carry (and applying them would recurse into the token fetch).

Changes

  • generators/rust/base/src/asIs/http_client.rs — build the token request and run it through apply_custom_headers before executing; documented why request-level and auth headers stay out.
  • Unit test in the same file (test_oauth_token_request_sends_custom_headers): binds a one-shot loopback TCP server, drives fetch_oauth_token against it, and asserts the raw wire bytes contain the client's custom header. No new dependencies (tokio is already a full-feature dep of generated SDKs).
  • Regenerated seed output: 142 seed/rust-sdk/**/src/core/http_client.rs + 36 seed/cli/**/src/core/http_client.rs (the CLI generator embeds the Rust SDK's asIs files).
  • Rust SDK changelog entry (fix).

Test plan

  • pnpm seed test --generator rust-sdk --local --skip-scripts → 142/142 pass
  • pnpm seed test --generator cli --local --skip-scripts → 141/141 pass
  • cargo test on a Rust seed fixture is blocked in this environment (crates.io is not reachable), so the new unit test has not been executed locally — relying on CI for that.

Reported by a customer whose gateway requires a custom auth header on the token endpoint; the generated SDK could not complete the OAuth exchange at all.


Open in Devin Review

@fern-api
fern-api Bot requested a review from iamnamananand996 as a code owner July 30, 2026 13:51

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

The core fix is right: build the token request and run it through apply_custom_headers before execute, with a clear rationale for excluding request-level and auth headers. Main risks are in the new unit test that now ships into every generated SDK: it depends on tokio's net/io-util features being enabled in the generated Cargo.toml, and the read loop can hang with no timeout. Comments target generators/rust/base/src/asIs/http_client.rs; the same points apply to all 178 regenerated seed copies.

  • 🟡 2 warning(s)
  • 🔵 2 suggestion(s)

Comment on lines +800 to +806
#[tokio::test]
async fn test_oauth_token_request_sends_custom_headers() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let base_url = format!("http://{}", listener.local_addr().expect("addr"));
let server = tokio::spawn(serve_one_token_request(listener));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

This test ships into every generated SDK, so it hard-depends on tokio being available at test time with the net, io-util, rt-multi-thread and macros features. If any generated Cargo.toml template pins tokio with a narrower feature set (or as a non-dev dependency only), cargo test breaks for all consumers. Given cargo test couldn't be run locally, please confirm the generated Cargo.toml feature list before merging — a compile failure here lands in 178 SDKs at once.

Comment on lines +780 to +786
loop {
let read = socket.read(&mut buffer).await.expect("read");
raw.extend_from_slice(&buffer[..read]);
if read == 0 || String::from_utf8_lossy(&raw).contains("\r\n\r\n") {
break;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

The read loop has no timeout: if a client ever sends headers without a terminating \r\n\r\n and keeps the connection open, this hangs forever and takes CI with it. Wrap the accept/read in tokio::time::timeout (or at least bound the loop iterations) so a failure surfaces as a test failure rather than a hung job.

let raw_request = server.await.expect("server");
assert_eq!(access_token, "token-from-server");
assert!(
raw_request.contains("x-gateway-token: sunflower"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

Asserting the exact lowercase wire form couples the test to hyper's header serialization. A case-insensitive check is more robust:

Suggested change
raw_request.contains("x-gateway-token: sunflower"),
raw_request.to_ascii_lowercase().contains("x-gateway-token: sunflower"),


let (access_token, _) = client
.fetch_oauth_token(
&client.config.base_url.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

&client.config.base_url.clone() allocates a String just to take a &str of it. &client.config.base_url works via deref coercion (and avoids a redundant_clone clippy hit).

@devin-ai-integration devin-ai-integration Bot 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@github-actions

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-30T05:00:19Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
rust-sdk square 213s (n=5) 182s (n=5) 211s -2s (-0.9%)

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-07-30T05:00:19Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-30 14:15 UTC

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