ConfigFetch.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. // Copyright 2024 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseCore
  15. import Foundation
  16. #if SWIFT_PACKAGE
  17. @_implementationOnly import GoogleUtilities_NSData
  18. #else
  19. import FirebaseInstallations
  20. import FirebaseRemoteConfigInterop
  21. @_implementationOnly import GoogleUtilities
  22. #endif // SWIFT_PACKAGE
  23. // TODO(ncooke3): Once Obj-C tests are ported, all `public` access modifers can be removed.
  24. #if RCN_STAGING_SERVER
  25. private let serverURLDomain = "staging-firebaseremoteconfig.sandbox.googleapis.com"
  26. #else
  27. private let serverURLDomain = "firebaseremoteconfig.googleapis.com"
  28. #endif
  29. private let requestJSONKeyAppID = "app_id"
  30. private let eTagHeaderName = "Etag"
  31. /// Remote Config Error Info End Time Seconds;
  32. private let throttledEndTimeInSecondsKey = "error_throttled_end_time_seconds"
  33. /// Fetch identifier for Base Fetch
  34. private let baseFetchType = "BASE"
  35. /// Fetch identifier for Realtime Fetch
  36. private let realtimeFetchType = "REALTIME"
  37. /// HTTP status codes. Ref: https://cloud.google.com/apis/design/errors#error_retries
  38. private enum FetchResponseStatus: Int {
  39. case ok = 200
  40. case tooManyRequests = 429
  41. case internalError = 500
  42. case serviceUnavailable = 503
  43. case gatewayTimeout = 504
  44. }
  45. // MARK: - Dependency Injection Protocols
  46. @objc public protocol RCNURLSessionDataTaskProtocol {
  47. func resume()
  48. }
  49. extension URLSessionDataTask: RCNURLSessionDataTaskProtocol {}
  50. @objc public protocol RCNConfigFetchSession {
  51. var configuration: URLSessionConfiguration { get }
  52. func invalidateAndCancel()
  53. @preconcurrency
  54. func dataTask(with request: URLRequest,
  55. completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void)
  56. -> RCNURLSessionDataTaskProtocol
  57. }
  58. extension URLSession: RCNConfigFetchSession {
  59. public func dataTask(with request: URLRequest,
  60. completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?)
  61. -> Void) -> any RCNURLSessionDataTaskProtocol {
  62. let dataTask: URLSessionDataTask = dataTask(with: request, completionHandler: completionHandler)
  63. return dataTask as RCNURLSessionDataTaskProtocol
  64. }
  65. }
  66. // MARK: - ConfigFetch
  67. @objc(RCNConfigFetch) public class ConfigFetch: NSObject {
  68. private let content: ConfigContent
  69. let settings: ConfigSettings
  70. private let analytics: (any FIRAnalyticsInterop)?
  71. private let experiment: ConfigExperiment?
  72. /// Guard the read/write operation.
  73. private let lockQueue: DispatchQueue
  74. public var installations: (any InstallationsProtocol)?
  75. /// Provide fetchSession for tests to override.
  76. /// - Note: Managed internally by the fetch instance.
  77. public var fetchSession: any RCNConfigFetchSession
  78. private let namespace: String
  79. private let options: FirebaseOptions
  80. /// Provide config template version number for Realtime config client.
  81. @objc public var templateVersionNumber: String
  82. @objc public convenience init(content: ConfigContent,
  83. DBManager: ConfigDBManager,
  84. settings: ConfigSettings,
  85. analytics: (any FIRAnalyticsInterop)?,
  86. experiment: ConfigExperiment?,
  87. queue: DispatchQueue,
  88. namespace: String,
  89. options: FirebaseOptions) {
  90. self.init(
  91. content: content,
  92. DBManager: DBManager,
  93. settings: settings,
  94. analytics: analytics,
  95. experiment: experiment,
  96. queue: queue,
  97. namespace: namespace,
  98. options: options,
  99. fetchSessionProvider: URLSession.init(configuration:),
  100. installations: nil
  101. )
  102. }
  103. private let configuredFetchSessionProvider: (ConfigSettings) -> RCNConfigFetchSession
  104. /// Designated initializer
  105. @objc public init(content: ConfigContent,
  106. DBManager: ConfigDBManager,
  107. settings: ConfigSettings,
  108. analytics: (any FIRAnalyticsInterop)?,
  109. experiment: ConfigExperiment?,
  110. queue: DispatchQueue,
  111. namespace: String,
  112. options: FirebaseOptions,
  113. fetchSessionProvider: @escaping (URLSessionConfiguration)
  114. -> RCNConfigFetchSession,
  115. installations: InstallationsProtocol?) {
  116. self.namespace = namespace
  117. self.settings = settings
  118. self.analytics = analytics
  119. self.experiment = experiment
  120. lockQueue = queue
  121. self.content = content
  122. configuredFetchSessionProvider = { settings in
  123. let config = URLSessionConfiguration.default
  124. config.timeoutIntervalForRequest = settings.fetchTimeout
  125. config.timeoutIntervalForResource = settings.fetchTimeout
  126. return fetchSessionProvider(config)
  127. }
  128. fetchSession = configuredFetchSessionProvider(settings)
  129. self.options = options
  130. templateVersionNumber = settings.lastFetchedTemplateVersion
  131. self.installations = if let installations {
  132. installations
  133. } else if
  134. let appName = namespace.components(separatedBy: ":").last,
  135. let app = FirebaseApp.app(name: appName) {
  136. Installations.installations(app: app)
  137. } else {
  138. nil as InstallationsProtocol?
  139. }
  140. super.init()
  141. }
  142. public var disableNetworkSessionRecreation: Bool = false
  143. /// Add the ability to update NSURLSession's timeout after a session has already been created.
  144. @objc public func recreateNetworkSession() {
  145. if disableNetworkSessionRecreation {
  146. return
  147. }
  148. fetchSession.invalidateAndCancel()
  149. fetchSession = configuredFetchSessionProvider(settings)
  150. }
  151. /// Return the current session. (Tests).
  152. @objc public func currentNetworkSession() -> RCNConfigFetchSession {
  153. fetchSession
  154. }
  155. deinit {
  156. fetchSession.invalidateAndCancel()
  157. }
  158. // MARK: - Fetch Config API
  159. /// Fetches config data keyed by namespace. Completion block will be called on the main queue.
  160. /// - Parameters:
  161. /// - expirationDuration: Expiration duration, in seconds.
  162. /// - completionHandler: Callback handler.
  163. @objc public func fetchConfig(withExpirationDuration expirationDuration: TimeInterval,
  164. completionHandler: ((RemoteConfigFetchStatus, (any Error)?)
  165. -> Void)?) {
  166. // Note: We expect the googleAppID to always be available.
  167. let hasDeviceContextChanged = Device.remoteConfigHasDeviceContextChanged(
  168. settings.deviceContext,
  169. projectIdentifier: options.googleAppID
  170. )
  171. lockQueue.async { [weak self] in
  172. guard let strongSelf = self else { return }
  173. // Check whether we are outside of the minimum fetch interval.
  174. if !strongSelf.settings
  175. .hasMinimumFetchIntervalElapsed(expirationDuration) && !hasDeviceContextChanged {
  176. RCLog.debug("I-RCN000051", "Returning cached data.")
  177. strongSelf.reportCompletion(on: completionHandler, status: .success, error: nil)
  178. return
  179. }
  180. // Check if a fetch is already in progress.
  181. if strongSelf.settings.isFetchInProgress {
  182. // Check if we have some fetched data.
  183. if strongSelf.settings.lastFetchTimeInterval > 0 {
  184. RCLog.debug(
  185. "I-RCN000052",
  186. "A fetch is already in progress. Using previous fetch results."
  187. )
  188. strongSelf
  189. .reportCompletion(
  190. on: completionHandler,
  191. status: strongSelf.settings.lastFetchStatus,
  192. error: nil
  193. )
  194. return
  195. } else {
  196. RCLog.error("I-RCN000053", "A fetch is already in progress. Ignoring duplicate request.")
  197. strongSelf.reportCompletion(on: completionHandler, status: .failure, error: nil)
  198. return
  199. }
  200. }
  201. // Check whether cache data is within throttle limit.
  202. if strongSelf.settings.shouldThrottle() && !hasDeviceContextChanged {
  203. // Must set lastFetchStatus before FailReason.
  204. strongSelf.settings.lastFetchStatus = .throttled
  205. strongSelf.settings.lastFetchError = RemoteConfigError.throttled
  206. let throttledEndTime = strongSelf.settings.exponentialBackoffThrottleEndTime
  207. let error = NSError(
  208. domain: ConfigConstants.remoteConfigErrorDomain,
  209. code: RemoteConfigError.throttled.rawValue,
  210. userInfo: [throttledEndTimeInSecondsKey: throttledEndTime]
  211. )
  212. strongSelf
  213. .reportCompletion(
  214. on: completionHandler,
  215. status: strongSelf.settings.lastFetchStatus,
  216. error: error
  217. )
  218. return
  219. }
  220. strongSelf.settings.isFetchInProgress = true
  221. let fetchTypeHeader = "\(baseFetchType)/1"
  222. strongSelf
  223. .refreshInstallationsToken(
  224. withFetchHeader: fetchTypeHeader,
  225. completionHandler: completionHandler,
  226. updateCompletionHandler: nil
  227. )
  228. }
  229. }
  230. // MARK: - Fetch Helpers
  231. /// Fetches config data immediately, keyed by namespace. Completion block will be called on the
  232. /// main queue.
  233. /// - Parameters:
  234. /// - fetchAttemptNumber: The number of the fetch attempt.
  235. /// - completionHandler: Callback handler.
  236. @objc public func realtimeFetchConfig(fetchAttemptNumber: Int,
  237. completionHandler: @escaping (RemoteConfigFetchStatus,
  238. RemoteConfigUpdate?,
  239. Error?) -> Void) {
  240. // Note: We expect the googleAppID to always be available.
  241. let hasDeviceContextChanged = Device.remoteConfigHasDeviceContextChanged(
  242. settings.deviceContext,
  243. projectIdentifier: options.googleAppID
  244. )
  245. lockQueue.async { [weak self] in
  246. guard let strongSelf = self else { return }
  247. // Check whether cache data is within throttle limit.
  248. if strongSelf.settings.shouldThrottle() && !hasDeviceContextChanged {
  249. // Must set lastFetchStatus before FailReason.
  250. strongSelf.settings.lastFetchStatus = .throttled
  251. strongSelf.settings.lastFetchError = RemoteConfigError.throttled
  252. let throttledEndTime = strongSelf.settings.exponentialBackoffThrottleEndTime
  253. let error = NSError(
  254. domain: ConfigConstants.remoteConfigErrorDomain,
  255. code: RemoteConfigError.throttled.rawValue,
  256. userInfo: [throttledEndTimeInSecondsKey: throttledEndTime]
  257. )
  258. strongSelf
  259. .reportCompletion(
  260. status: .failure,
  261. update: nil,
  262. error: error,
  263. completionHandler: nil,
  264. updateCompletionHandler: completionHandler
  265. )
  266. return
  267. }
  268. strongSelf.settings.isFetchInProgress = true
  269. let fetchTypeHeader = "\(realtimeFetchType)/\(fetchAttemptNumber)"
  270. strongSelf
  271. .refreshInstallationsToken(
  272. withFetchHeader: fetchTypeHeader,
  273. completionHandler: nil,
  274. updateCompletionHandler: completionHandler
  275. )
  276. }
  277. }
  278. /// Refresh installation ID token before fetching config. installation ID is now mandatory for
  279. /// fetch requests to work.(b/14751422).
  280. private func refreshInstallationsToken(withFetchHeader fetchTypeHeader: String,
  281. completionHandler: (
  282. (RemoteConfigFetchStatus, Error?) -> Void
  283. )?,
  284. updateCompletionHandler: (
  285. (RemoteConfigFetchStatus, RemoteConfigUpdate?, Error?)
  286. -> Void
  287. )?) {
  288. guard let installations, !options.gcmSenderID.isEmpty else {
  289. let errorDescription = "Failed to get GCMSenderID"
  290. RCLog.error("I-RCN000074", errorDescription)
  291. settings.isFetchInProgress = false
  292. reportCompletion(
  293. on: completionHandler,
  294. status: .failure,
  295. error: NSError(
  296. domain: ConfigConstants.remoteConfigErrorDomain,
  297. code: RemoteConfigError.internalError.rawValue,
  298. userInfo: [NSLocalizedDescriptionKey: errorDescription]
  299. )
  300. )
  301. return
  302. }
  303. let installationsTokenHandler: (InstallationsAuthTokenResult?, (any Error)?)
  304. -> Void = { [weak self] tokenResult, error in
  305. guard let strongSelf = self else { return }
  306. // NOTE(ncooke3): Confirmed that tokenResult is nil.
  307. if let error {
  308. let errorDescription = "Failed to get installations token. Error : \(error)."
  309. RCLog.error("I-RCN000073", errorDescription)
  310. strongSelf.settings.isFetchInProgress = false
  311. let userInfo: [String: Any] = [
  312. NSLocalizedDescriptionKey: errorDescription,
  313. NSUnderlyingErrorKey: (error as NSError).userInfo[NSUnderlyingErrorKey] as Any,
  314. ]
  315. strongSelf.reportCompletion(
  316. on: completionHandler,
  317. status: .failure,
  318. error: NSError(
  319. domain: ConfigConstants.remoteConfigErrorDomain,
  320. code: RemoteConfigError.internalError.rawValue,
  321. userInfo: userInfo
  322. )
  323. )
  324. return
  325. }
  326. // We have a valid token. Get the backing installationID.
  327. installations.installationID { [weak self] identifier, error in
  328. guard let strongSelf = self else { return }
  329. // Dispatch to the RC serial queue to update settings on the queue.
  330. strongSelf.lockQueue.async { [weak self] in
  331. guard let strongSelf = self else { return }
  332. // Update config settings with the IID and token.
  333. strongSelf.settings.configInstallationsToken = tokenResult?.authToken
  334. strongSelf.settings.configInstallationsIdentifier = identifier ?? ""
  335. // NOTE(ncooke3): Confirmed that identifier is nil.
  336. if let error {
  337. let errorDescription = "Error getting iid : \(error.localizedDescription)"
  338. let userInfo: [String: Any] = [
  339. NSLocalizedDescriptionKey: errorDescription,
  340. NSUnderlyingErrorKey: (error as NSError).userInfo[NSUnderlyingErrorKey] as Any,
  341. ]
  342. RCLog.error("I-RCN000055", errorDescription)
  343. strongSelf.settings.isFetchInProgress = false
  344. strongSelf.reportCompletion(
  345. on: completionHandler,
  346. status: .failure,
  347. error: NSError(
  348. domain: ConfigConstants.remoteConfigErrorDomain,
  349. code: RemoteConfigError.internalError.rawValue,
  350. userInfo: userInfo
  351. )
  352. )
  353. return
  354. }
  355. RCLog
  356. .info(
  357. "I-RCN000022",
  358. "Success to get iid : \(strongSelf.settings.configInstallationsIdentifier)."
  359. )
  360. strongSelf.doFetchCall(
  361. fetchTypeHeader: fetchTypeHeader,
  362. completionHandler: completionHandler,
  363. updateCompletionHandler: updateCompletionHandler
  364. )
  365. }
  366. }
  367. }
  368. RCLog.debug("I-RCN000039", "Starting requesting token.")
  369. installations.authToken(completion: installationsTokenHandler)
  370. }
  371. private func doFetchCall(fetchTypeHeader: String,
  372. completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  373. updateCompletionHandler: (
  374. (RemoteConfigFetchStatus, RemoteConfigUpdate?, Error?) -> Void
  375. )?) {
  376. getAnalyticsUserProperties { userProperties in
  377. self.lockQueue.async {
  378. self.fetch(
  379. userProperties: userProperties,
  380. fetchTypeHeader: fetchTypeHeader,
  381. completionHandler: completionHandler,
  382. updateCompletionHandler: updateCompletionHandler
  383. )
  384. }
  385. }
  386. }
  387. private func getAnalyticsUserProperties(completionHandler: @escaping ([String: Any]) -> Void) {
  388. RCLog.debug("I-RCN000060", "Fetch with user properties completed.")
  389. if analytics == nil {
  390. completionHandler([:])
  391. } else {
  392. analytics?.getUserProperties(callback: completionHandler)
  393. }
  394. }
  395. private func reportCompletion(on handler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  396. status: RemoteConfigFetchStatus,
  397. error: Error?) {
  398. reportCompletion(
  399. status: status,
  400. update: nil,
  401. error: error,
  402. completionHandler: handler,
  403. updateCompletionHandler: nil
  404. )
  405. }
  406. private func reportCompletion(status: RemoteConfigFetchStatus,
  407. update: RemoteConfigUpdate?,
  408. error: Error?,
  409. completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  410. updateCompletionHandler: (
  411. (RemoteConfigFetchStatus, RemoteConfigUpdate?, Error?) -> Void
  412. )?) {
  413. if let completionHandler {
  414. DispatchQueue.main.async {
  415. completionHandler(status, error)
  416. }
  417. }
  418. // if completion handler expects a config update response
  419. if let updateCompletionHandler {
  420. DispatchQueue.main.async {
  421. updateCompletionHandler(status, update, error)
  422. }
  423. }
  424. }
  425. private func fetch(userProperties: [String: Any],
  426. fetchTypeHeader: String,
  427. completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  428. updateCompletionHandler: (
  429. (RemoteConfigFetchStatus, RemoteConfigUpdate?, Error?) -> Void
  430. )?) {
  431. RCLog.debug("I-RCN000061", "Fetch with user properties initiated.")
  432. let postRequestString = settings.nextRequest(withUserProperties: userProperties)
  433. // Get POST request content.
  434. guard
  435. let content = postRequestString.data(using: .utf8),
  436. let compressedContent = try? NSData.gul_data(byGzippingData: content)
  437. else {
  438. let errorString = "Failed to compress the config request."
  439. RCLog.warning("I-RCN000033", errorString)
  440. let error = NSError(
  441. domain: ConfigConstants.remoteConfigErrorDomain,
  442. code: RemoteConfigError.internalError.rawValue,
  443. userInfo: [NSLocalizedDescriptionKey: errorString]
  444. )
  445. settings.isFetchInProgress = false
  446. reportCompletion(
  447. status: .failure,
  448. update: nil,
  449. error: error,
  450. completionHandler: completionHandler,
  451. updateCompletionHandler: updateCompletionHandler
  452. )
  453. return
  454. }
  455. RCLog.debug("I-RCN000040", "Start config fetch.")
  456. let fetcherCompletion: (Data?, URLResponse?, Error?) -> Void = {
  457. [weak self] data,
  458. response,
  459. error in
  460. RCLog.debug(
  461. "I-RCN000050",
  462. "Config fetch completed. Error: \(error?.localizedDescription ?? "nil") StatusCode: \((response as? HTTPURLResponse)?.statusCode ?? 0)"
  463. )
  464. guard let strongSelf = self else { return }
  465. // The fetch has completed.
  466. strongSelf.settings.isFetchInProgress = false
  467. strongSelf.lockQueue.async { [weak self] in
  468. guard let strongSelf = self else { return }
  469. let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
  470. if error != nil || statusCode != FetchResponseStatus.ok.rawValue {
  471. // Update metadata about fetch failure.
  472. strongSelf.settings.updateMetadata(withFetchSuccessStatus: false, templateVersion: nil)
  473. if let error {
  474. if strongSelf.settings.lastFetchStatus == .success {
  475. RCLog.error(
  476. "I-RCN000025",
  477. "RCN Fetch failure: \(error.localizedDescription). Using cached config result."
  478. )
  479. } else {
  480. RCLog.error(
  481. "I-RCN000026",
  482. "RCN Fetch failure: \(error.localizedDescription). No cached config result."
  483. )
  484. }
  485. }
  486. if statusCode != FetchResponseStatus.ok.rawValue {
  487. RCLog.error("I-RCN000026", "RCN Fetch failure. Response HTTP error code: \(statusCode)")
  488. if statusCode == FetchResponseStatus.tooManyRequests
  489. .rawValue || statusCode == FetchResponseStatus.internalError
  490. .rawValue || statusCode == FetchResponseStatus.serviceUnavailable
  491. .rawValue || statusCode == FetchResponseStatus.gatewayTimeout.rawValue {
  492. strongSelf.settings.updateExponentialBackoffTime()
  493. if strongSelf.settings.shouldThrottle() {
  494. // Must set lastFetchStatus before FailReason.
  495. strongSelf.settings.lastFetchStatus = .throttled
  496. strongSelf.settings.lastFetchError = RemoteConfigError.throttled
  497. let throttledEndTime = strongSelf.settings.exponentialBackoffThrottleEndTime
  498. let error = NSError(
  499. domain: ConfigConstants.remoteConfigErrorDomain,
  500. code: RemoteConfigError.throttled.rawValue,
  501. userInfo: [throttledEndTimeInSecondsKey: throttledEndTime]
  502. )
  503. strongSelf
  504. .reportCompletion(
  505. status: strongSelf.settings.lastFetchStatus,
  506. update: nil,
  507. error: error,
  508. completionHandler: completionHandler,
  509. updateCompletionHandler: updateCompletionHandler
  510. )
  511. return
  512. }
  513. }
  514. }
  515. // Return back the received error.
  516. // Must set lastFetchStatus before setting Fetch Error.
  517. strongSelf.settings.lastFetchStatus = .failure
  518. strongSelf.settings.lastFetchError = .internalError
  519. let userInfo: [String: Any] = [
  520. NSUnderlyingErrorKey: error ?? "Missing error.",
  521. NSLocalizedDescriptionKey: error?
  522. .localizedDescription ?? "Internal Error. Status code: \(statusCode)",
  523. ]
  524. strongSelf.reportCompletion(
  525. status: .failure,
  526. update: nil,
  527. error: NSError(
  528. domain: ConfigConstants.remoteConfigErrorDomain,
  529. code: RemoteConfigError.internalError.rawValue,
  530. userInfo: userInfo
  531. ),
  532. completionHandler: completionHandler,
  533. updateCompletionHandler: updateCompletionHandler
  534. )
  535. return
  536. }
  537. // Fetch was successful. Check if we have data.
  538. guard let data else {
  539. RCLog.info("I-RCN000043", "RCN Fetch: No data in fetch response")
  540. // There may still be a difference between fetched and active config
  541. let update = strongSelf.content.getConfigUpdate(forNamespace: strongSelf.namespace)
  542. strongSelf
  543. .reportCompletion(
  544. status: .success,
  545. update: update,
  546. error: nil,
  547. completionHandler: completionHandler,
  548. updateCompletionHandler: updateCompletionHandler
  549. )
  550. return
  551. }
  552. // Config fetch succeeded.
  553. // JSONObjectWithData is always expected to return an NSDictionary in our case
  554. do {
  555. let fetchedConfig = try JSONSerialization.jsonObject(
  556. with: data,
  557. options: .mutableContainers
  558. ) as? [String: Any]
  559. // Check and log if we received an error from the server
  560. if
  561. let fetchedConfig,
  562. fetchedConfig.count == 1,
  563. let errDict = fetchedConfig[ConfigConstants.fetchResponseKeyError] as? [String: Any] {
  564. var errStr = "RCN Fetch Failure: Server returned error:"
  565. if let errorCode = errDict[ConfigConstants.fetchResponseKeyErrorCode] {
  566. errStr = errStr.appending("Code: \(errorCode)")
  567. }
  568. if let errorStatus = errDict[ConfigConstants.fetchResponseKeyErrorStatus] {
  569. errStr = errStr.appending(". Status: \(errorStatus)")
  570. }
  571. if let errorMessage = errDict[ConfigConstants.fetchResponseKeyErrorMessage] {
  572. errStr = errStr.appending(". Message: \(errorMessage)")
  573. }
  574. RCLog.error("I-RCN000044", errStr + ".")
  575. let error = NSError(
  576. domain: ConfigConstants.remoteConfigErrorDomain,
  577. code: RemoteConfigError.internalError.rawValue,
  578. userInfo: [NSLocalizedDescriptionKey: errStr]
  579. )
  580. strongSelf
  581. .reportCompletion(
  582. status: .failure,
  583. update: nil,
  584. error: error,
  585. completionHandler: completionHandler,
  586. updateCompletionHandler: updateCompletionHandler
  587. )
  588. return
  589. }
  590. // Add the fetched config to the database.
  591. if let fetchedConfig {
  592. // Update config content to cache and DB.
  593. strongSelf.content
  594. .updateConfigContent(withResponse: fetchedConfig, forNamespace: strongSelf.namespace)
  595. // Update experiments only for 3p namespace
  596. let namespace = strongSelf.namespace.components(separatedBy: ":")[0]
  597. if namespace == RemoteConfigConstants.NamespaceGoogleMobilePlatform {
  598. let experiments =
  599. fetchedConfig[ConfigConstants
  600. .fetchResponseKeyExperimentDescriptions] as? [[String: Any]]
  601. strongSelf.experiment?.updateExperiments(withResponse: experiments)
  602. }
  603. strongSelf.templateVersionNumber = strongSelf
  604. .getTemplateVersionNumber(fetchedConfig: fetchedConfig)
  605. } else {
  606. RCLog.debug("I-RCN000063", "Empty response with no fetched config.")
  607. }
  608. // We had a successful fetch. Update the current Etag in settings if different.
  609. // Look for "Etag" but fall back to "etag" if needed.
  610. let latestETag = (response as? HTTPURLResponse)?
  611. .allHeaderFields[eTagHeaderName] as? String ?? (response as? HTTPURLResponse)?
  612. .allHeaderFields["etag"] as? String
  613. if strongSelf.settings.lastETag == nil ||
  614. strongSelf.settings.lastETag != latestETag {
  615. strongSelf.settings.lastETag = latestETag
  616. }
  617. // Compute config update after successful fetch
  618. let update = strongSelf.content.getConfigUpdate(forNamespace: strongSelf.namespace)
  619. strongSelf.settings.updateMetadata(
  620. withFetchSuccessStatus: true,
  621. templateVersion: strongSelf.templateVersionNumber
  622. )
  623. strongSelf
  624. .reportCompletion(
  625. status: .success,
  626. update: update,
  627. error: nil,
  628. completionHandler: completionHandler,
  629. updateCompletionHandler: updateCompletionHandler
  630. )
  631. return
  632. } catch {
  633. RCLog.error(
  634. "I-RCN000042",
  635. "RCN Fetch failure: \(error). Could not parse response data as JSON"
  636. )
  637. }
  638. }
  639. }
  640. RCLog.debug("I-RCN000061", "Making remote config fetch.")
  641. let dataTask = urlSessionDataTask(content: compressedContent,
  642. fetchTypeHeader: fetchTypeHeader,
  643. completionHandler: fetcherCompletion)
  644. dataTask.resume()
  645. }
  646. private static func newFetchSession(settings: ConfigSettings) -> URLSession {
  647. let config = URLSessionConfiguration.default
  648. config.timeoutIntervalForRequest = settings.fetchTimeout
  649. config.timeoutIntervalForResource = settings.fetchTimeout
  650. let session = URLSession(configuration: config)
  651. return session
  652. }
  653. private func urlSessionDataTask(content: Data,
  654. fetchTypeHeader: String,
  655. completionHandler fetcherCompletion: @escaping (Data?,
  656. URLResponse?,
  657. Error?) -> Void)
  658. -> RCNURLSessionDataTaskProtocol {
  659. let url = Utils.constructServerURL(
  660. domain: serverURLDomain,
  661. apiKey: options.apiKey,
  662. optionsID: options.projectID ?? "",
  663. namespace: namespace
  664. )
  665. RCLog.debug("I-RCN000046", "Making config request: \(url.absoluteString)")
  666. let timeoutInterval = fetchSession.configuration.timeoutIntervalForResource
  667. var urlRequest = URLRequest(url: url,
  668. cachePolicy: .reloadIgnoringLocalCacheData,
  669. timeoutInterval: timeoutInterval)
  670. urlRequest.httpMethod = "POST"
  671. urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
  672. urlRequest.setValue(settings.configInstallationsToken,
  673. forHTTPHeaderField: "x-goog-firebase-installations-auth")
  674. urlRequest.setValue(
  675. Bundle.main.bundleIdentifier,
  676. forHTTPHeaderField: "X-Ios-Bundle-Identifier"
  677. )
  678. urlRequest.setValue("gzip", forHTTPHeaderField: "Content-Encoding")
  679. urlRequest.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
  680. urlRequest.setValue(fetchTypeHeader, forHTTPHeaderField: "X-Firebase-RC-Fetch-Type")
  681. if let etag = settings.lastETag {
  682. urlRequest.setValue(etag, forHTTPHeaderField: "if-none-match")
  683. }
  684. urlRequest.httpBody = content
  685. return fetchSession.dataTask(with: urlRequest, completionHandler: fetcherCompletion)
  686. }
  687. private func getTemplateVersionNumber(fetchedConfig: [String: Any]) -> String {
  688. if let templateVersion =
  689. fetchedConfig[ConfigConstants.fetchResponseKeyTemplateVersion] as? String {
  690. return templateVersion
  691. }
  692. return "0"
  693. }
  694. }