Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(api): support custom request headers by mikepitre · Pull Request #729 · clerk/clerk-android · GitHub
Skip to content

feat(api): support custom request headers - #729

Merged
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers
Jun 22, 2026
Merged

feat(api): support custom request headers#729
swolfand merged 2 commits into
mainfrom
mike/custom-request-headers

Conversation

@mikepitre

Copy link
Copy Markdown
Collaborator

Summary

  • add an options-owned API for configuring additional Clerk API request headers
  • thread those headers through ConfigurationManager and ClerkApi into the outgoing request middleware
  • add focused coverage for option propagation and request header appending

Why

@clerk/expo needs to identify itself when it configures the native Android SDK, in addition to the existing Android SDK headers. The motivating headers are:

  • x-clerk-host-sdk: expo
  • x-clerk-host-sdk-version: <expo package version>

This mirrors the direction available on iOS, where request customization lives in Clerk.Options middleware.

Approach

This keeps the new capability part of ClerkConfigurationOptions, but adds it as an additive method:

ClerkConfigurationOptions()
.withCustomHeaders(
mapOf(
"x-clerk-host-sdk" to "expo",
"x-clerk-host-sdk-version" to "3.4.3",
)
)

The configured headers are snapshotted with toMap() and appended by VersioningUserAgentMiddleware after the built-in Clerk headers. Existing headers like x-android-sdk-version, x-mobile, auth, client id, and device id are left intact.

Compatibility and tradeoffs

I intentionally did not add customHeaders as a new primary-constructor property on ClerkConfigurationOptions. Even with a default value, changing a Kotlin data-class primary constructor can be source-compatible while still being binary-risky for already-compiled consumers because constructor/default-arg/copy signatures change.

The tradeoff is that customHeaders is not part of the data class generated copy(), equals(), or hashCode() semantics in this non-major version. That keeps the change additive and minor-safe while still making headers option-owned. In a future major, we can move customHeaders into the primary constructor for the cleaner Kotlin data-class shape.

Testing

  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:compileDebugKotlin :source:api:compileDebugUnitTestKotlin

Focused Robolectric test execution was attempted, but this local machine only has Java 17 installed and Robolectric selected Android SDK 36, which requires Java 21. The changed source and test Kotlin compile successfully.

@coderabbitai

coderabbitaiBot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@swolfand, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0724d61-5538-48c0-a071-dc1fcfb0349d

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b4f and 1063ef0.

📒 Files selected for processing (2)
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
📝 Walkthrough

Walkthrough

ClerkConfigurationOptions gains a customHeaders: Map<String, String> property and a withCustomHeaders(...) copy-style method. ConfigurationManager.configureSdkState passes these headers into ClerkApi.configure via a new optional customHeaders parameter. ClerkApi stores them in an internal configuredCustomHeaders property, resets them on reset(), and forwards them to buildRetrofit. buildRetrofit constructs VersioningUserAgentMiddleware with the header map, which stores it and appends each entry to outgoing OkHttp requests. New unit tests cover both the middleware header-appending behavior and the end-to-end propagation from options to ClerkApi.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately and concisely describes the main change: adding support for custom request headers in the API configuration.
Description check✅ PassedThe description comprehensively covers the summary, motivation, approach, design tradeoffs, and testing performed, providing clear context for the feature.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt (1)

110-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear _magicLink during reset to avoid stale API usage.

Line [110]-[122] resets most Retrofit services, but _magicLink is not nulled. That leaves one endpoint callable against stale configuration after reset().

Proposed fix
 fun reset() {
_client = null
_environment = null
_session = null
_signIn = null
_signUp = null
_user = null
_deviceAttestation = null
_organization = null
+ _magicLink = null
configuredBaseUrl = null
configuredUrlWithVersion = null
configuredCustomHeaders = emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt` around lines
110 - 122, The reset() function in the ClerkApi class clears most Retrofit
service properties but does not null the _magicLink property, leaving it
callable with stale configuration. Add _magicLink = null to the reset() function
alongside the other property resets to ensure all API endpoints are properly
cleared during reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`:
- Line 55: The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.
---
Outside diff comments:
In `@source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt`:
- Around line 110-122: The reset() function in the ClerkApi class clears most
Retrofit service properties but does not null the _magicLink property, leaving
it callable with stale configuration. Add _magicLink = null to the reset()
function alongside the other property resets to ensure all API endpoints are
properly cleared during reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d12540e-e8da-425b-ae6f-238657d05961

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3638a and 2366b4f.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sdk/ProxyUrlConfigurationTest.kt

newRequestBuilder.removeHeader("Content-Type")
}

customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OkHttp Request.Builder, what is the exact behavior difference between addHeader(name, value) and header(name, value) when the header already exists?

💡 Result:

In the OkHttp Request.Builder, the difference between header(name, value) and addHeader(name, value) lies in how they handle existing headers with the same name: 1. header(name, value): This method sets the specified header to the given value [1][2]. If any headers with that name already exist, they are all removed and replaced by this single new value [1][2]. Use this method when you want to ensure the header has exactly one value [3]. 2. addHeader(name, value): This method appends a new header with the specified name and value to the request [1][2]. If headers with that name already exist, they are preserved, and the new value is added alongside them [4]. This method is preferred for headers that support multiple values, such as "Cookie" [1][2]. Additionally, note that for certain headers, such as Content-Length and Content-Encoding, OkHttp may override the provided value with a header derived from the request body, regardless of which method is used [1][2].

Citations:


🏁 Script executed:

# Find and examine the VersioningUserAgentMiddleware file
find . -name "VersioningUserAgentMiddleware.kt" -type f

Repository: clerk/clerk-android

Length of output: 167


🏁 Script executed:

# Examine the middleware implementation
cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt

Repository: clerk/clerk-android

Length of output: 3073


🏁 Script executed:

# Check for OutgoingHeaders enum definition
find . -name "OutgoingHeaders.kt" -o -name "*OutgoingHeaders*"| head -5

Repository: clerk/clerk-android

Length of output: 45


🏁 Script executed:

# Search for tests of VersioningUserAgentMiddleware
find . -path "*/test*" -name "*VersioningUserAgent*" -type f

Repository: clerk/clerk-android

Length of output: 169


🏁 Script executed:

cat -n ./source/api/src/test/java/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddlewareTest.kt

Repository: clerk/clerk-android

Length of output: 6898


🏁 Script executed:

# Search for where VersioningUserAgentMiddleware is instantiated with customHeaders
rg "VersioningUserAgentMiddleware" -A 2 -B 2 --type kotlin

Repository: clerk/clerk-android

Length of output: 3451


🏁 Script executed:

# Check case sensitivity of OutgoingHeaders - check if header names use exact case
rg "addHeader|header\(" source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt -A 1

Repository: clerk/clerk-android

Length of output: 819


Prevent custom headers from overriding reserved Clerk authentication and metadata headers.

Line 55 uses addHeader for arbitrary custom headers without filtering. If customHeaders contains keys matching reserved headers (case-insensitive HTTP semantics: Authorization, x-clerk-client-id, x-clerk-device-id, etc.), OkHttp will create duplicate header entries instead of replacing them. This can cause conflicting authentication tokens or client identifiers in outgoing requests.

Additionally, the test at line 77–86 only validates non-reserved custom headers and does not cover the collision scenario.

Proposed fix
 internal class VersioningUserAgentMiddleware(customHeaders: Map<String, String> = emptyMap()) :
Interceptor {
private val customHeaders = customHeaders.toMap()
+ private val reservedHeaders =+ setOf(+ OutgoingHeaders.CLERK_API_VERSION.header,+ OutgoingHeaders.X_ANDROID_SDK_VERSION.header,+ OutgoingHeaders.X_MOBILE.header,+ OutgoingHeaders.AUTHORIZATION.header,+ OutgoingHeaders.X_CLERK_CLIENT_ID.header,+ OutgoingHeaders.X_CLERK_DEVICE_ID.header,+ ).map { it.lowercase() }.toSet()
override fun intercept(chain: Interceptor.Chain): Response {
@@
- customHeaders.forEach { (name, value) -> newRequestBuilder.addHeader(name, value) }+ customHeaders.forEach { (name, value) ->+ if (name.lowercase() !in reservedHeaders) {+ newRequestBuilder.addHeader(name, value)+ }+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/VersioningUserAgentMiddleware.kt`
at line 55, The customHeaders forEach loop at line 55 in
VersioningUserAgentMiddleware uses addHeader to apply all custom headers without
filtering, which allows reserved Clerk authentication headers (Authorization,
x-clerk-client-id, x-clerk-device-id, etc.) to be duplicated via
case-insensitive HTTP semantics rather than replaced. Filter the customHeaders
before the forEach loop to exclude any keys matching reserved headers (perform
case-insensitive comparison), then only add the filtered headers using
addHeader. Additionally, update the test at lines 77-86 to validate that custom
headers matching reserved header names are properly excluded and do not create
duplicate header entries in the request.

@mikepitre
mikepitre requested a review from swolfandJune 16, 2026 13:16
@mikepitre

mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
CollaboratorAuthor

@swolfand if you think there's a better way to do this, feel free to do that. Just need the ability to add custom headers to outgoing requests. iOS lets you inject custom middleware.

@swolfand
swolfand merged commit 2c9c588 into mainJun 22, 2026
10 checks passed
@swolfand
swolfand deleted the mike/custom-request-headers branch June 22, 2026 17:50
Sign up for freeto 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.

2 participants

@mikepitre@swolfand