RetryPolicy.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. //
  2. // RetryPolicy.swift
  3. //
  4. // Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. import Foundation
  25. /// A retry policy that retries requests using an exponential backoff for allowed HTTP methods and HTTP status codes
  26. /// as well as certain types of networking errors.
  27. open class RetryPolicy: RequestInterceptor {
  28. /// The default retry limit for retry policies.
  29. public static let defaultRetryLimit: UInt = 2
  30. /// The default exponential backoff base for retry policies (must be a minimum of 2).
  31. public static let defaultExponentialBackoffBase: UInt = 2
  32. /// The default exponential backoff scale for retry policies.
  33. public static let defaultExponentialBackoffScale: Double = 0.5
  34. /// The default HTTP methods to retry.
  35. /// See [RFC 2616 - Section 9.1.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) for more information.
  36. public static let defaultRetryableHTTPMethods: Set<HTTPMethod> = [
  37. .delete, // [Delete](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7) - not always idempotent
  38. .get, // [GET](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3) - generally idempotent
  39. .head, // [HEAD](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4) - generally idempotent
  40. .options, // [OPTIONS](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2) - inherently idempotent
  41. .put, // [PUT](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6) - not always idempotent
  42. .trace // [TRACE](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8) - inherently idempotent
  43. ]
  44. /// The default HTTP status codes to retry.
  45. /// See [RFC 2616 - Section 10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10) for more information.
  46. public static let defaultRetryableHTTPStatusCodes: Set<Int> = [
  47. 408, // [Request Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.9)
  48. 500, // [Internal Server Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1)
  49. 502, // [Bad Gateway](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.3)
  50. 503, // [Service Unavailable](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4)
  51. 504 // [Gateway Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.5)
  52. ]
  53. /// The default URL error codes to retry.
  54. public static let defaultRetryableURLErrorCodes: Set<URLError.Code> = [
  55. // [Security] App Transport Security disallowed a connection because there is no secure network connection.
  56. // - [Disabled] ATS settings do not change at runtime.
  57. //.appTransportSecurityRequiresSecureConnection,
  58. // [System] An app or app extension attempted to connect to a background session that is already connected to a
  59. // process.
  60. // - [Enabled] The other process could release the background session.
  61. .backgroundSessionInUseByAnotherProcess,
  62. // [System] The shared container identifier of the URL session configuration is needed but has not been set.
  63. // - [Disabled] Cannot change at runtime.
  64. //.backgroundSessionRequiresSharedContainer,
  65. // [System] The app is suspended or exits while a background data task is processing.
  66. // - [Enabled] App can be foregrounded or launched to recover.
  67. .backgroundSessionWasDisconnected,
  68. // [Network] The URL Loading system received bad data from the server.
  69. // - [Enabled] Server could return valid data when retrying.
  70. .badServerResponse,
  71. // [Resource] A malformed URL prevented a URL request from being initiated.
  72. // - [Disabled] URL was most likely constructed incorrectly.
  73. //.badURL,
  74. // [System] A connection was attempted while a phone call is active on a network that does not support
  75. // simultaneous phone and data communication (EDGE or GPRS).
  76. // - [Enabled] Phone call could be ended to allow request to recover.
  77. .callIsActive,
  78. // [Client] An asynchronous load has been canceled.
  79. // - [Disabled] Request was cancelled by the client.
  80. //.cancelled,
  81. // [File System] A download task couldn’t close the downloaded file on disk.
  82. // - [Disabled] File system error is unlikely to recover with retry.
  83. //.cannotCloseFile,
  84. // [Network] An attempt to connect to a host failed.
  85. // - [Enabled] Server or DNS lookup could recover during retry.
  86. .cannotConnectToHost,
  87. // [File System] A download task couldn’t create the downloaded file on disk because of an I/O failure.
  88. // - [Disabled] File system error is unlikely to recover with retry.
  89. //.cannotCreateFile,
  90. // [Data] Content data received during a connection request had an unknown content encoding.
  91. // - [Disabled] Server is unlikely to modify the content encoding during a retry.
  92. //.cannotDecodeContentData,
  93. // [Data] Content data received during a connection request could not be decoded for a known content encoding.
  94. // - [Disabled] Server is unlikely to modify the content encoding during a retry.
  95. //.cannotDecodeRawData,
  96. // [Network] The host name for a URL could not be resolved.
  97. // - [Enabled] Server or DNS lookup could recover during retry.
  98. .cannotFindHost,
  99. // [Network] A request to load an item only from the cache could not be satisfied.
  100. // - [Enabled] Cache could be populated during a retry.
  101. .cannotLoadFromNetwork,
  102. // [File System] A download task was unable to move a downloaded file on disk.
  103. // - [Disabled] File system error is unlikely to recover with retry.
  104. //.cannotMoveFile,
  105. // [File System] A download task was unable to open the downloaded file on disk.
  106. // - [Disabled] File system error is unlikely to recover with retry.
  107. //.cannotOpenFile,
  108. // [Data] A task could not parse a response.
  109. // - [Disabled] Invalid response is unlikely to recover with retry.
  110. //.cannotParseResponse,
  111. // [File System] A download task was unable to remove a downloaded file from disk.
  112. // - [Disabled] File system error is unlikely to recover with retry.
  113. //.cannotRemoveFile,
  114. // [File System] A download task was unable to write to the downloaded file on disk.
  115. // - [Disabled] File system error is unlikely to recover with retry.
  116. //.cannotWriteToFile,
  117. // [Security] A client certificate was rejected.
  118. // - [Disabled] Client certificate is unlikely to change with retry.
  119. //.clientCertificateRejected,
  120. // [Security] A client certificate was required to authenticate an SSL connection during a request.
  121. // - [Disabled] Client certificate is unlikely to be provided with retry.
  122. //.clientCertificateRequired,
  123. // [Data] The length of the resource data exceeds the maximum allowed.
  124. // - [Disabled] Resource will likely still exceed the length maximum on retry.
  125. //.dataLengthExceedsMaximum,
  126. // [System] The cellular network disallowed a connection.
  127. // - [Enabled] WiFi connection could be established during retry.
  128. .dataNotAllowed,
  129. // [Network] The host address could not be found via DNS lookup.
  130. // - [Enabled] DNS lookup could succeed during retry.
  131. .dnsLookupFailed,
  132. // [Data] A download task failed to decode an encoded file during the download.
  133. // - [Enabled] Server could correct the decoding issue with retry.
  134. .downloadDecodingFailedMidStream,
  135. // [Data] A download task failed to decode an encoded file after downloading.
  136. // - [Enabled] Server could correct the decoding issue with retry.
  137. .downloadDecodingFailedToComplete,
  138. // [File System] A file does not exist.
  139. // - [Disabled] File system error is unlikely to recover with retry.
  140. //.fileDoesNotExist,
  141. // [File System] A request for an FTP file resulted in the server responding that the file is not a plain file,
  142. // but a directory.
  143. // - [Disabled] FTP directory is not likely to change to a file during a retry.
  144. //.fileIsDirectory,
  145. // [Network] A redirect loop has been detected or the threshold for number of allowable redirects has been
  146. // exceeded (currently 16).
  147. // - [Disabled] The redirect loop is unlikely to be resolved within the retry window.
  148. //.httpTooManyRedirects,
  149. // [System] The attempted connection required activating a data context while roaming, but international roaming
  150. // is disabled.
  151. // - [Enabled] WiFi connection could be established during retry.
  152. .internationalRoamingOff,
  153. // [Connectivity] A client or server connection was severed in the middle of an in-progress load.
  154. // - [Enabled] A network connection could be established during retry.
  155. .networkConnectionLost,
  156. // [File System] A resource couldn’t be read because of insufficient permissions.
  157. // - [Disabled] Permissions are unlikely to be granted during retry.
  158. //.noPermissionsToReadFile,
  159. // [Connectivity] A network resource was requested, but an internet connection has not been established and
  160. // cannot be established automatically.
  161. // - [Enabled] A network connection could be established during retry.
  162. .notConnectedToInternet,
  163. // [Resource] A redirect was specified by way of server response code, but the server did not accompany this
  164. // code with a redirect URL.
  165. // - [Disabled] The redirect URL is unlikely to be supplied during a retry.
  166. //.redirectToNonExistentLocation,
  167. // [Client] A body stream is needed but the client did not provide one.
  168. // - [Disabled] The client will be unlikely to supply a body stream during retry.
  169. //.requestBodyStreamExhausted,
  170. // [Resource] A requested resource couldn’t be retrieved.
  171. // - [Disabled] The resource is unlikely to become available during the retry window.
  172. //.resourceUnavailable,
  173. // [Security] An attempt to establish a secure connection failed for reasons that can’t be expressed more
  174. // specifically.
  175. // - [Enabled] The secure connection could be established during a retry given the lack of specificity
  176. // provided by the error.
  177. .secureConnectionFailed,
  178. // [Security] A server certificate had a date which indicates it has expired, or is not yet valid.
  179. // - [Enabled] The server certificate could become valid within the retry window.
  180. .serverCertificateHasBadDate,
  181. // [Security] A server certificate was not signed by any root server.
  182. // - [Disabled] The server certificate is unlikely to change during the retry window.
  183. //.serverCertificateHasUnknownRoot,
  184. // [Security] A server certificate is not yet valid.
  185. // - [Enabled] The server certificate could become valid within the retry window.
  186. .serverCertificateNotYetValid,
  187. // [Security] A server certificate was signed by a root server that isn’t trusted.
  188. // - [Disabled] The server certificate is unlikely to become trusted within the retry window.
  189. //.serverCertificateUntrusted,
  190. // [Network] An asynchronous operation timed out.
  191. // - [Enabled] The request timed out for an unknown reason and should be retried.
  192. .timedOut
  193. // [System] The URL Loading System encountered an error that it can’t interpret.
  194. // - [Disabled] The error could not be interpreted and is unlikely to be recovered from during a retry.
  195. //.unknown,
  196. // [Resource] A properly formed URL couldn’t be handled by the framework.
  197. // - [Disabled] The URL is unlikely to change during a retry.
  198. //.unsupportedURL,
  199. // [Client] Authentication is required to access a resource.
  200. // - [Disabled] The user authentication is unlikely to be provided by retrying.
  201. //.userAuthenticationRequired,
  202. // [Client] An asynchronous request for authentication has been canceled by the user.
  203. // - [Disabled] The user cancelled authentication and explicitly took action to not retry.
  204. //.userCancelledAuthentication,
  205. // [Resource] A server reported that a URL has a non-zero content length, but terminated the network connection
  206. // gracefully without sending any data.
  207. // - [Disabled] The server is unlikely to provide data during the retry window.
  208. //.zeroByteResource,
  209. ]
  210. /// The total number of times the request is allowed to be retried.
  211. public let retryLimit: UInt
  212. /// The base of the exponential backoff policy (should always be greater than or equal to 2).
  213. public let exponentialBackoffBase: UInt
  214. /// The scale of the exponential backoff.
  215. public let exponentialBackoffScale: Double
  216. /// The HTTP methods that are allowed to be retried.
  217. public let retryableHTTPMethods: Set<HTTPMethod>
  218. /// The HTTP status codes that are automatically retried by the policy.
  219. public let retryableHTTPStatusCodes: Set<Int>
  220. /// The URL error codes that are automatically retried by the policy.
  221. public let retryableURLErrorCodes: Set<URLError.Code>
  222. /// Creates an `ExponentialBackoffRetryPolicy` from the specified parameters.
  223. ///
  224. /// - Parameters:
  225. /// - retryLimit: The total number of times the request is allowed to be retried. `2` by default.
  226. /// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default.
  227. /// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default.
  228. /// - retryableHTTPMethods: The HTTP methods that are allowed to be retried.
  229. /// `RetryPolicy.defaultRetryableHTTPMethods` by default.
  230. /// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy.
  231. /// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default.
  232. /// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy.
  233. /// `RetryPolicy.defaultRetryableURLErrorCodes` by default.
  234. public init(
  235. retryLimit: UInt = RetryPolicy.defaultRetryLimit,
  236. exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase,
  237. exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale,
  238. retryableHTTPMethods: Set<HTTPMethod> = RetryPolicy.defaultRetryableHTTPMethods,
  239. retryableHTTPStatusCodes: Set<Int> = RetryPolicy.defaultRetryableHTTPStatusCodes,
  240. retryableURLErrorCodes: Set<URLError.Code> = RetryPolicy.defaultRetryableURLErrorCodes)
  241. {
  242. precondition(exponentialBackoffBase >= 2, "The `exponentialBackoffBase` must be a minimum of 2.")
  243. self.retryLimit = retryLimit
  244. self.exponentialBackoffBase = exponentialBackoffBase
  245. self.exponentialBackoffScale = exponentialBackoffScale
  246. self.retryableHTTPMethods = retryableHTTPMethods
  247. self.retryableHTTPStatusCodes = retryableHTTPStatusCodes
  248. self.retryableURLErrorCodes = retryableURLErrorCodes
  249. }
  250. open func retry(
  251. _ request: Request,
  252. for session: Session,
  253. dueTo error: Error,
  254. completion: @escaping (RetryResult) -> Void)
  255. {
  256. if
  257. request.retryCount < retryLimit,
  258. let httpMethod = request.request?.method,
  259. retryableHTTPMethods.contains(httpMethod),
  260. shouldRetry(response: request.response, error: error)
  261. {
  262. let timeDelay = pow(Double(exponentialBackoffBase), Double(request.retryCount)) * exponentialBackoffScale
  263. completion(.retryWithDelay(timeDelay))
  264. } else {
  265. completion(.doNotRetry)
  266. }
  267. }
  268. private func shouldRetry(response: HTTPURLResponse?, error: Error) -> Bool {
  269. if let statusCode = response?.statusCode, retryableHTTPStatusCodes.contains(statusCode) {
  270. return true
  271. } else if let errorCode = (error as? URLError)?.code, retryableURLErrorCodes.contains(errorCode) {
  272. return true
  273. }
  274. return false
  275. }
  276. }
  277. // MARK: -
  278. /// A retry policy that automatically retries idempotent requests for network connection lost errors. For more
  279. /// information about retrying network connection lost errors, please refer to Apple's
  280. /// [technical document](https://developer.apple.com/library/content/qa/qa1941/_index.html).
  281. open class ConnectionLostRetryPolicy: RetryPolicy {
  282. /// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters.
  283. ///
  284. /// - Parameters:
  285. /// - retryLimit: The total number of times the request is allowed to be retried.
  286. /// `RetryPolicy.defaultRetryLimit` by default.
  287. /// - exponentialBackoffBase: The base of the exponential backoff policy.
  288. /// `RetryPolicy.defaultExponentialBackoffBase` by default.
  289. /// - exponentialBackoffScale: The scale of the exponential backoff.
  290. /// `RetryPolicy.defaultExponentialBackoffScale` by default.
  291. /// - retryableHTTPMethods: The idempotent http methods to retry.
  292. /// `RetryPolicy.defaultRetryableHTTPMethods` by default.
  293. public init(
  294. retryLimit: UInt = RetryPolicy.defaultRetryLimit,
  295. exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase,
  296. exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale,
  297. retryableHTTPMethods: Set<HTTPMethod> = RetryPolicy.defaultRetryableHTTPMethods)
  298. {
  299. super.init(
  300. retryLimit: retryLimit,
  301. exponentialBackoffBase: exponentialBackoffBase,
  302. exponentialBackoffScale: exponentialBackoffScale,
  303. retryableHTTPMethods: retryableHTTPMethods,
  304. retryableHTTPStatusCodes: [],
  305. retryableURLErrorCodes: [.networkConnectionLost]
  306. )
  307. }
  308. }