Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions FlagsmithClient/Classes/Flagsmith.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ public final class Flagsmith: @unchecked Sendable {
}
}

/// Custom HTTP headers to inject into every Flagsmith request.
///
/// The closure is invoked **fresh** on every request, so dynamic values
/// (like OAuth Bearer tokens that get refreshed) are always up to date.
///
/// Example — inject an Authorization header:
/// ```swift
/// Flagsmith.shared.customHeaders = { [weak tokenStore] in
/// guard let token = tokenStore?.tokens?.accessToken else { return [:] }
/// return ["Authorization": "Bearer \(token)"]
/// }
/// ```
public var customHeaders: (@Sendable () -> [String: String])?

/// Is flag analytics enabled?
public var enableAnalytics: Bool {
get { analytics.enableAnalytics }
Expand Down
7 changes: 7 additions & 0 deletions FlagsmithClient/Classes/Internal/APIManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ final class APIManager: NSObject, URLSessionDataDelegate, @unchecked Sendable {
return
}

// Inject custom headers (called fresh per request to support dynamic values like OAuth tokens)
if let headers = Flagsmith.shared.customHeaders?() {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}

// set the cache policy based on Flagsmith settings
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
if Flagsmith.shared.cacheConfig.useCache {
Expand Down
7 changes: 7 additions & 0 deletions FlagsmithClient/Classes/Internal/SSEManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ final class SSEManager: NSObject, URLSessionDataDelegate, @unchecked Sendable {
request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
request.setValue("keep-alive", forHTTPHeaderField: "Connection")

// Inject custom headers (called fresh per request to support dynamic values like OAuth tokens)
if let headers = Flagsmith.shared.customHeaders?() {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}

completionHandler = completion
dataTask = session.dataTask(with: request)
dataTask?.resume()
Expand Down
66 changes: 66 additions & 0 deletions FlagsmithClient/Tests/CustomHeadersTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// CustomHeadersTests.swift
// FlagsmithClientTests
//

@testable import FlagsmithClient
import XCTest

final class CustomHeadersTests: FlagsmithClientTestCase {
override func tearDown() {
super.tearDown()
Flagsmith.shared.customHeaders = nil
}

/// Verify the `customHeaders` closure is invoked when a request is made.
func testCustomHeadersClosureIsInvoked() throws {
let closureInvoked = expectation(description: "customHeaders closure invoked")

Flagsmith.shared.customHeaders = {
closureInvoked.fulfill()
return ["X-Test-Header": "value"]
}
Flagsmith.shared.apiKey = "mock-test-api-key"
// Force a quick failure so the request doesn't hang
Flagsmith.shared.baseURL = URL(fileURLWithPath: "/dev/null")

Flagsmith.shared.getFeatureFlags { _ in }

wait(for: [closureInvoked], timeout: 1.0)
}

/// Verify that when `customHeaders` is nil, requests still work normally.
func testNilCustomHeadersDoesNotCrash() throws {
Flagsmith.shared.customHeaders = nil
Flagsmith.shared.apiKey = "mock-test-api-key"
Flagsmith.shared.baseURL = URL(fileURLWithPath: "/dev/null")

let requestFinished = expectation(description: "Request finished without crash")

Flagsmith.shared.getFeatureFlags { _ in
requestFinished.fulfill()
}

wait(for: [requestFinished], timeout: 1.0)
}

/// Verify the closure is invoked on every request (not cached).
func testCustomHeadersClosureInvokedEveryRequest() throws {
var invocationCount = 0
Flagsmith.shared.customHeaders = {
invocationCount += 1
return [:]
}
Flagsmith.shared.apiKey = "mock-test-api-key"
Flagsmith.shared.baseURL = URL(fileURLWithPath: "/dev/null")

let firstRequest = expectation(description: "First request")
let secondRequest = expectation(description: "Second request")

Flagsmith.shared.getFeatureFlags { _ in firstRequest.fulfill() }
Flagsmith.shared.getFeatureFlags { _ in secondRequest.fulfill() }

wait(for: [firstRequest, secondRequest], timeout: 2.0)
XCTAssertEqual(invocationCount, 2, "customHeaders should be invoked for every request")
}
}