mobile-sdk-ios icon indicating copy to clipboard operation
mobile-sdk-ios copied to clipboard

API requests may be sent without custom User-Agent header in certain scenarios

Open andrii-bodnar opened this issue 5 months ago • 0 comments

API requests (including /api/v2/languages) can be sent without a custom User-Agent header in certain scenarios, which may cause issues with API rate limiting, analytics, or server-side filtering.

When This Occurs

The versioned() method in CrowdinAPI.swift can fail to set the User-Agent header in two scenarios:

  1. Bundle identifier not found: When the SDK is integrated via Swift Package Manager (SPM) instead of CocoaPods, the bundle identifier "org.cocoapods.CrowdinSDK" may not exist, causing the guard statement to return early without setting the User-Agent.

  2. macOS not handled: The code only handles iOS, tvOS, and watchOS platforms. When running on macOS, the #if conditions don't match, so the User-Agent header is never set.

Affected Code

func versioned(_ headers: [String: String]?) -> [String: String] {
    var result = headers ?? [:]
    guard let bundle = Bundle(identifier: "org.cocoapods.CrowdinSDK"), 
          let sdkVersionNumber = bundle.infoDictionary?["CFBundleShortVersionString"] as? String 
    else { return result }  // ⚠️ Returns without User-Agent if bundle not found
#if os(iOS) || os(tvOS)
    let systemVersion = "iOS: \(UIDevice.current.systemVersion)"
    result["User-Agent"] = "crowdin-ios-sdk/\(sdkVersionNumber) iOS/\(systemVersion)"
#elseif os(watchOS)
    let systemVersion = "watchOS: \(WKInterfaceDevice.current().systemVersion)"
    result["User-Agent"] = "crowdin-ios-sdk/\(sdkVersionNumber) iOS/\(systemVersion)"
#endif
    // ⚠️ No User-Agent set for macOS
    return result
}

Location: Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift:215-226

Impact

  • All API endpoints using CrowdinAPI (including /api/v2/languages) may send requests without a custom User-Agent
  • This affects both async (cw_get, cw_post, etc.) and sync (cw_getSync, cw_postSync, etc.) methods
  • Requests will fall back to the system default User-Agent, which may not identify the SDK properly

Suggested Fix

  1. Add fallback logic to find the bundle using alternative methods (e.g., Bundle(for: type(of: self)) or Bundle.main)
  2. Add macOS support to the platform-specific code
  3. Ensure a User-Agent is always set, even if the SDK version cannot be determined (e.g., use a default value)

Example Fix Approach

Note: The example code below should be verified and tested before implementation.

func versioned(_ headers: [String: String]?) -> [String: String] {
    var result = headers ?? [:]
    
    // Try multiple bundle lookup strategies
    var bundle: Bundle?
    var sdkVersionNumber: String?
    
    // Try CocoaPods bundle first
    if let podBundle = Bundle(identifier: "org.cocoapods.CrowdinSDK"),
       let version = podBundle.infoDictionary?["CFBundleShortVersionString"] as? String {
        bundle = podBundle
        sdkVersionNumber = version
    }
    // Fallback to SPM bundle
    else if let spmBundle = Bundle(for: type(of: self)),
             let version = spmBundle.infoDictionary?["CFBundleShortVersionString"] as? String {
        bundle = spmBundle
        sdkVersionNumber = version
    }
    
    let versionString = sdkVersionNumber ?? "unknown"
    
    #if os(iOS) || os(tvOS)
    let systemVersion = "iOS: \(UIDevice.current.systemVersion)"
    result["User-Agent"] = "crowdin-ios-sdk/\(versionString) iOS/\(systemVersion)"
    #elseif os(watchOS)
    let systemVersion = "watchOS: \(WKInterfaceDevice.current().systemVersion)"
    result["User-Agent"] = "crowdin-ios-sdk/\(versionString) iOS/\(systemVersion)"
    #elseif os(macOS)
    let systemVersion = ProcessInfo.processInfo.operatingSystemVersionString
    result["User-Agent"] = "crowdin-ios-sdk/\(versionString) macOS/\(systemVersion)"
    #endif
    
    return result
}

Testing

  • Test with CocoaPods integration
  • Test with Swift Package Manager integration
  • Test on macOS platform
  • Verify User-Agent header is always present in network requests

andrii-bodnar avatar Nov 13 '25 13:11 andrii-bodnar