未验证 提交 4b2c33cb 编写于 作者: C Christian Noon 提交者: GitHub

Added response serializer retry support (#2716)

* [WIP] First stab at retry support for response serialization

* Refactored response serialization retry logic to always run all serializers

* Refactored RequestDelegate retry APIs so retry calls are explicit

* Removed need for response serialzer index for iteration

* Fixed issue where next serializer was being triggered during retry

* Modified request reset to be called when retrying in case of adapt errors

* Added tests around response serializer retry logic and adaptation failures

* Applied same queue cleanup to non-serialized response APIs

* Moved response serializer APIs to be internal instead of open

* Refactored retry APIs, simplifying guards, and removed RetryResult optional

* Replaced another conditional with a guard in retry logic
上级 34b43bd7
......@@ -68,10 +68,6 @@ open class Request {
/// The `Request`'s delegate.
public weak var delegate: RequestDelegate?
/// `OperationQueue` used internally to enqueue response callbacks. Starts suspended but is activated when the
/// `Request` is finished.
let internalQueue: OperationQueue
// MARK: - Updated State
/// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`.
......@@ -86,6 +82,10 @@ open class Request {
var redirectHandler: RedirectHandler?
/// `CachedResponseHandler` provided to handle caching responses.
var cachedResponseHandler: CachedResponseHandler?
/// Response serialization closures that handle parsing responses.
var responseSerializers: [() -> Void] = []
/// Response serialization completion closures executed once all response serialization is complete.
var responseSerializerCompletions: [() -> Void] = []
/// `URLCredential` used for authentication challenges.
var credential: URLCredential?
/// All `URLRequest`s created by Alamofire on behalf of the `Request`.
......@@ -237,10 +237,6 @@ open class Request {
self.id = id
self.underlyingQueue = underlyingQueue
self.serializationQueue = serializationQueue
internalQueue = OperationQueue(maxConcurrentOperationCount: 1,
underlyingQueue: underlyingQueue,
name: "org.alamofire.request-\(id)",
startSuspended: true)
self.eventMonitor = eventMonitor
self.interceptor = interceptor
self.delegate = delegate
......@@ -293,14 +289,12 @@ open class Request {
retryOrFinish(error: error)
}
/// Called when a `URLSessionTask` is created on behalf of the `Request`. Calls `reset()`.
/// Called when a `URLSessionTask` is created on behalf of the `Request`.
///
/// - Parameter task: The `URLSessionTask` created.
func didCreateTask(_ task: URLSessionTask) {
protectedMutableState.write { $0.tasks.append(task) }
reset()
eventMonitor?.request(self, didCreateTask: task)
}
......@@ -346,20 +340,26 @@ open class Request {
retryOrFinish(error: self.error)
}
/// Called when the `RequestDelegate` is retrying this `Request`.
func requestIsRetrying() {
/// Called when the `RequestDelegate` is going to retry this `Request`. Calls `reset()`.
func prepareForRetry() {
protectedMutableState.write { $0.retryCount += 1 }
reset()
eventMonitor?.requestIsRetrying(self)
}
/// Called to trigger retry or finish this `Request`.
func retryOrFinish(error: Error?) {
if let error = error, delegate?.willAttemptToRetryRequest(self) == true {
delegate?.retryRequest(self, ifNecessaryWithError: error)
return
} else {
finish()
guard let error = error, let delegate = delegate else { finish(); return }
delegate.retryResult(for: self, dueTo: error) { retryResult in
switch retryResult {
case .doNotRetry, .doNotRetryWithError:
self.finish(error: retryResult.error)
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
}
......@@ -368,12 +368,53 @@ open class Request {
if let error = error { self.error = error }
// Start response handlers
internalQueue.isSuspended = false
processNextResponseSerializer()
eventMonitor?.requestDidFinish(self)
}
/// Resets all task related state for retry.
/// Appends the response serialization closure to the `Request`.
func appendResponseSerializer(_ closure: @escaping () -> Void) {
protectedMutableState.write { $0.responseSerializers.append(closure) }
}
/// Returns the next response serializer closure to execute if there's one left.
func nextResponseSerializer() -> (() -> Void)? {
var responseSerializer: (() -> Void)?
protectedMutableState.write { mutableState in
let responseSerializerIndex = mutableState.responseSerializerCompletions.count
if responseSerializerIndex < mutableState.responseSerializers.count {
responseSerializer = mutableState.responseSerializers[responseSerializerIndex]
}
}
return responseSerializer
}
/// Processes the next response serializer and calls all completions if response serialization is complete.
func processNextResponseSerializer() {
guard let responseSerializer = nextResponseSerializer() else {
// Execute all response serializer completions
protectedMutableState.directValue.responseSerializerCompletions.forEach { $0() }
// Cleanup the request
cleanup()
return
}
serializationQueue.async { responseSerializer() }
}
/// Notifies the `Request` that the response serializer is complete.
func responseSerializerDidComplete(completion: @escaping () -> Void) {
protectedMutableState.write { $0.responseSerializerCompletions.append(completion) }
processNextResponseSerializer()
}
/// Resets all task and response serializer related state for retry.
func reset() {
error = nil
......@@ -381,6 +422,8 @@ open class Request {
uploadProgress.completedUnitCount = 0
downloadProgress.totalUnitCount = 0
downloadProgress.completedUnitCount = 0
protectedMutableState.write { $0.responseSerializerCompletions = [] }
}
/// Called when updating the upload progress.
......@@ -535,6 +578,13 @@ open class Request {
return self
}
// MARK: - Cleanup
/// Final cleanup step executed when a `Request` finishes response serialization.
open func cleanup() {
// No-op: override in subclass
}
}
// MARK: - Protocol Conformances
......@@ -650,8 +700,8 @@ extension Request: CustomDebugStringConvertible {
public protocol RequestDelegate: AnyObject {
var sessionConfiguration: URLSessionConfiguration { get }
func willAttemptToRetryRequest(_ request: Request) -> Bool
func retryRequest(_ request: Request, ifNecessaryWithError error: Error)
func retryResult(for request: Request, dueTo error: Error, completion: @escaping (RetryResult) -> Void)
func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?)
func cancelRequest(_ request: Request)
func cancelDownloadRequest(_ request: DownloadRequest, byProducingResumeData: @escaping (Data?) -> Void)
......@@ -949,17 +999,6 @@ open class UploadRequest: DataRequest {
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
// Automatically remove temporary upload files (e.g. multipart form data)
internalQueue.addOperation {
guard
let uploadable = self.uploadable,
case let .file(url, shouldRemove) = uploadable,
shouldRemove else { return }
// TODO: Abstract file manager
try? FileManager.default.removeItem(at: url)
}
}
func didCreateUploadable(_ uploadable: Uploadable) {
......@@ -1001,6 +1040,19 @@ open class UploadRequest: DataRequest {
return stream
}
open override func cleanup() {
super.cleanup()
guard
let uploadable = self.uploadable,
case let .file(url, shouldRemove) = uploadable,
shouldRemove
else { return }
// TODO: Abstract file manager
try? FileManager.default.removeItem(at: url)
}
}
public protocol UploadableConvertible {
......
......@@ -110,20 +110,19 @@ extension DataRequest {
/// - Returns: The request.
@discardableResult
public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<Data?>) -> Void) -> Self {
internalQueue.addOperation {
self.serializationQueue.async {
let result = Result(value: self.data, error: self.error)
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
(queue ?? .main).async { completionHandler(response) }
}
appendResponseSerializer {
let queue = queue ?? .main
let result = Result(value: self.data, error: self.error)
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
return self
......@@ -144,25 +143,57 @@ extension DataRequest {
completionHandler: @escaping (DataResponse<Serializer.SerializedObject>) -> Void)
-> Self
{
internalQueue.addOperation {
self.serializationQueue.async {
let start = CFAbsoluteTimeGetCurrent()
let result = Result { try responseSerializer.serialize(request: self.request,
response: self.response,
data: self.data,
error: self.error) }
let end = CFAbsoluteTimeGetCurrent()
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: (end - start),
result: result)
appendResponseSerializer {
let start = CFAbsoluteTimeGetCurrent()
let result = Result { try responseSerializer.serialize(request: self.request,
response: self.response,
data: self.data,
error: self.error) }
let end = CFAbsoluteTimeGetCurrent()
let queue: DispatchQueue = queue ?? .main
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: (end - start),
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
guard let serializerError = result.error, let delegate = self.delegate else {
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
return
}
self.eventMonitor?.request(self, didParseResponse: response)
delegate.retryResult(for: self, dueTo: serializerError) { retryResult in
var didComplete: (() -> Void)?
(queue ?? .main).async { completionHandler(response) }
defer {
if let didComplete = didComplete {
self.responseSerializerDidComplete { queue.async { didComplete() } }
}
}
switch retryResult {
case .doNotRetry:
didComplete = { completionHandler(response) }
case .doNotRetryWithError(let retryError):
let result = Result<Serializer.SerializedObject>.failure(retryError)
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: (end - start),
result: result)
didComplete = { completionHandler(response) }
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
}
......@@ -184,19 +215,18 @@ extension DownloadRequest {
completionHandler: @escaping (DownloadResponse<URL?>) -> Void)
-> Self
{
internalQueue.addOperation {
self.serializationQueue.async {
let result = Result(value: self.fileURL , error: self.error)
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: 0,
result: result)
appendResponseSerializer {
let queue = queue ?? .main
let result = Result(value: self.fileURL , error: self.error)
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: 0,
result: result)
(queue ?? .main).async { completionHandler(response) }
}
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
return self
......@@ -218,24 +248,57 @@ extension DownloadRequest {
completionHandler: @escaping (DownloadResponse<T.SerializedObject>) -> Void)
-> Self
{
internalQueue.addOperation {
self.serializationQueue.async {
let start = CFAbsoluteTimeGetCurrent()
let result = Result { try responseSerializer.serializeDownload(request: self.request,
response: self.response,
fileURL: self.fileURL,
error: self.error) }
let end = CFAbsoluteTimeGetCurrent()
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: (end - start),
result: result)
appendResponseSerializer {
let start = CFAbsoluteTimeGetCurrent()
let result = Result { try responseSerializer.serializeDownload(request: self.request,
response: self.response,
fileURL: self.fileURL,
error: self.error) }
let end = CFAbsoluteTimeGetCurrent()
let queue: DispatchQueue = queue ?? .main
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: (end - start),
result: result)
guard let serializerError = result.error, let delegate = self.delegate else {
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
return
}
delegate.retryResult(for: self, dueTo: serializerError) { retryResult in
var didComplete: (() -> Void)?
defer {
if let didComplete = didComplete {
self.responseSerializerDidComplete { queue.async { didComplete() } }
}
}
switch retryResult {
case .doNotRetry:
didComplete = { completionHandler(response) }
case .doNotRetryWithError(let retryError):
let result = Result<T.SerializedObject>.failure(retryError)
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: (end - start),
result: result)
didComplete = { completionHandler(response) }
(queue ?? .main).async { completionHandler(response) }
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
}
......
......@@ -513,41 +513,35 @@ extension Session: RequestDelegate {
return session.configuration
}
public func willAttemptToRetryRequest(_ request: Request) -> Bool {
return retrier(for: request) != nil
}
public func retryRequest(_ request: Request, ifNecessaryWithError error: Error) {
guard let retrier = retrier(for: request) else { request.finish(); return }
public func retryResult(for request: Request, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
guard let retrier = retrier(for: request) else {
rootQueue.async { completion(.doNotRetry) }
return
}
retrier.retry(request, for: self, dueTo: error) { result in
guard !request.isCancelled else { return }
self.rootQueue.async {
guard !request.isCancelled else { return }
if result.retryRequired {
let retry: () -> Void = {
guard !request.isCancelled else { return }
guard let retryResultError = result.error else { completion(result); return }
request.requestIsRetrying()
self.perform(request)
}
let retryError = AFError.requestRetryFailed(retryError: retryResultError, originalError: error)
completion(.doNotRetryWithError(retryError))
}
}
}
if let retryDelay = result.delay {
self.rootQueue.after(retryDelay) { retry() }
} else {
self.rootQueue.async { retry() }
}
} else {
var retryError = error
public func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) {
self.rootQueue.async {
let retry: () -> Void = {
guard !request.isCancelled else { return }
if let retryResultError = result.error {
retryError = AFError.requestRetryFailed(retryError: retryResultError, originalError: error)
}
request.prepareForRetry()
self.perform(request)
}
request.finish(error: retryError)
}
if let retryDelay = timeDelay {
self.rootQueue.after(retryDelay) { retry() }
} else {
retry()
}
}
}
......
......@@ -159,7 +159,7 @@ class RetryPolicyTestCase: BaseRetryPolicyTestCase {
waitForExpectations(timeout: timeout, handler: nil)
request.requestIsRetrying()
request.prepareForRetry()
}
// Then
......@@ -321,7 +321,7 @@ class RetryPolicyTestCase: BaseRetryPolicyTestCase {
waitForExpectations(timeout: timeout, handler: nil)
request.requestIsRetrying()
request.prepareForRetry()
}
// Then
......
......@@ -89,8 +89,10 @@ class SessionTestCase: BaseTestCase {
}
private class RequestHandler: RequestInterceptor {
var adaptCalledCount = 0
var adaptedCount = 0
var retryCount = 0
var retryCalledCount = 0
var retryErrors: [Error] = []
var shouldApplyAuthorizationHeader = false
......@@ -101,15 +103,17 @@ class SessionTestCase: BaseTestCase {
var retryDelay: TimeInterval?
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest>) -> Void) {
adaptCalledCount += 1
let result: Result<URLRequest> = Result {
if throwsErrorOnFirstAdapt {
throwsErrorOnFirstAdapt = false
throw AFError.invalidURL(url: "")
throw AFError.invalidURL(url: "/adapt/error/1")
}
if throwsErrorOnSecondAdapt && adaptedCount == 1 {
throwsErrorOnSecondAdapt = false
throw AFError.invalidURL(url: "")
throw AFError.invalidURL(url: "/adapt/error/2")
}
var urlRequest = urlRequest
......@@ -132,8 +136,10 @@ class SessionTestCase: BaseTestCase {
dueTo error: Error,
completion: @escaping (RetryResult) -> Void)
{
retryCalledCount += 1
if throwsErrorOnRetry {
let error = AFError.invalidURL(url: "")
let error = AFError.invalidURL(url: "/invalid/url/\(retryCalledCount)")
completion(.doNotRetryWithError(error))
return
}
......@@ -156,11 +162,15 @@ class SessionTestCase: BaseTestCase {
}
private class UploadHandler: RequestInterceptor {
var adaptCalledCount = 0
var adaptedCount = 0
var retryCalledCount = 0
var retryCount = 0
var retryErrors: [Error] = []
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest>) -> Void) {
adaptCalledCount += 1
let result: Result<URLRequest> = Result {
adaptedCount += 1
......@@ -178,6 +188,8 @@ class SessionTestCase: BaseTestCase {
dueTo error: Error,
completion: @escaping (RetryResult) -> Void)
{
retryCalledCount += 1
retryCount += 1
retryErrors.append(error)
......@@ -876,8 +888,10 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 2)
XCTAssertEqual(handler.retryCount, 2)
XCTAssertEqual(handler.retryCalledCount, 3)
XCTAssertEqual(handler.retryCount, 3)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
......@@ -904,10 +918,14 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(sessionHandler.adaptCalledCount, 3)
XCTAssertEqual(sessionHandler.adaptedCount, 3)
XCTAssertEqual(sessionHandler.retryCount, 2)
XCTAssertEqual(sessionHandler.retryCalledCount, 3)
XCTAssertEqual(sessionHandler.retryCount, 3)
XCTAssertEqual(requestHandler.adaptCalledCount, 3)
XCTAssertEqual(requestHandler.adaptedCount, 3)
XCTAssertEqual(requestHandler.retryCount, 3)
XCTAssertEqual(requestHandler.retryCalledCount, 4)
XCTAssertEqual(requestHandler.retryCount, 4)
XCTAssertEqual(request.retryCount, 2)
XCTAssertEqual(response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
......@@ -936,7 +954,9 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 2)
XCTAssertEqual(handler.retryCalledCount, 1)
XCTAssertEqual(handler.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, true)
XCTAssertTrue(session.requestTaskMap.isEmpty)
......@@ -970,7 +990,9 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 2)
XCTAssertEqual(handler.retryCalledCount, 1)
XCTAssertEqual(handler.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, true)
XCTAssertTrue(session.requestTaskMap.isEmpty)
......@@ -997,7 +1019,9 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 2)
XCTAssertEqual(handler.retryCalledCount, 1)
XCTAssertEqual(handler.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, true)
XCTAssertTrue(session.requestTaskMap.isEmpty)
......@@ -1024,7 +1048,9 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 2)
XCTAssertEqual(handler.retryCalledCount, 1)
XCTAssertEqual(handler.retryCount, 1)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, true)
......@@ -1052,15 +1078,17 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCount, 2)
XCTAssertEqual(handler.retryCalledCount, 3)
XCTAssertEqual(handler.retryCount, 3)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
if let error = response?.result.error?.asAFError {
XCTAssertTrue(error.isRequestAdaptationError)
XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "")
XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "/adapt/error/2")
} else {
XCTFail("error should not be nil")
}
......@@ -1088,15 +1116,17 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCount, 2)
XCTAssertEqual(handler.retryCalledCount, 3)
XCTAssertEqual(handler.retryCount, 3)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
if let error = response?.result.error?.asAFError {
XCTAssertTrue(error.isRequestAdaptationError)
XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "")
XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "/adapt/error/2")
} else {
XCTFail("error should not be nil")
}
......@@ -1123,7 +1153,9 @@ class SessionTestCase: BaseTestCase {
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 1)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCalledCount, 2)
XCTAssertEqual(handler.retryCount, 0)
XCTAssertEqual(request.retryCount, 0)
XCTAssertEqual(response?.result.isSuccess, false)
......@@ -1131,12 +1163,252 @@ class SessionTestCase: BaseTestCase {
if let error = response?.result.error?.asAFError {
XCTAssertTrue(error.isRequestRetryError)
XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "")
XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "/invalid/url/2")
} else {
XCTFail("error should not be nil")
}
}
// MARK: Tests - Response Serializer Retry
func testThatSessionCallsRequestRetrierWhenResponseSerializerThrowsError() {
// Given
let handler = RequestHandler()
handler.shouldRetry = false
let session = Session()
let expectation = self.expectation(description: "request should eventually fail")
var response: DataResponse<Any>?
// When
let request = session.request("https://httpbin.org/image/jpeg", interceptor: handler)
.validate()
.responseJSON { jsonResponse in
response = jsonResponse
expectation.fulfill()
}
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 1)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCalledCount, 1)
XCTAssertEqual(handler.retryCount, 0)
XCTAssertEqual(request.retryCount, 0)
XCTAssertEqual(response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
if let error = response?.error?.asAFError {
XCTAssertTrue(error.isResponseSerializationError)
XCTAssertTrue(error.localizedDescription.starts(with: "JSON could not be serialized"))
} else {
XCTFail("error should not be nil")
}
}
func testThatSessionCallsRequestRetrierForAllResponseSerializersThatThrowError() throws {
// Given
let handler = RequestHandler()
handler.throwsErrorOnRetry = true
let session = Session()
let json1Expectation = self.expectation(description: "request should eventually fail")
var json1Response: DataResponse<Any>?
let json2Expectation = self.expectation(description: "request should eventually fail")
var json2Response: DataResponse<Any>?
// When
let request = session.request("https://httpbin.org/image/jpeg", interceptor: handler)
.validate()
.responseJSON { response in
json1Response = response
json1Expectation.fulfill()
}
.responseJSON { response in
json2Response = response
json2Expectation.fulfill()
}
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 1)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCalledCount, 2)
XCTAssertEqual(handler.retryCount, 0)
XCTAssertEqual(request.retryCount, 0)
XCTAssertEqual(json1Response?.result.isSuccess, false)
XCTAssertEqual(json2Response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
let errors: [AFError] = [json1Response, json2Response].compactMap { $0?.error?.asAFError }
XCTAssertEqual(errors.count, 2)
for (index, error) in errors.enumerated() {
XCTAssertTrue(error.isRequestRetryError)
XCTAssertEqual(error.localizedDescription.starts(with: "Request retry failed with retry error"), true)
if case let .requestRetryFailed(retryError, originalError) = error {
XCTAssertEqual(try retryError.asAFError?.urlConvertible?.asURL().absoluteString, "/invalid/url/\(index + 1)")
XCTAssertTrue(originalError.localizedDescription.starts(with: "JSON could not be serialized"))
} else {
XCTFail("Error failure reason should be response serialization failure")
}
}
}
func testThatSessionRetriesRequestImmediatelyWhenResponseSerializerRequestsRetry() throws {
// Given
let handler = RequestHandler()
let session = Session()
let json1Expectation = self.expectation(description: "request should eventually fail")
var json1Response: DataResponse<Any>?
let json2Expectation = self.expectation(description: "request should eventually fail")
var json2Response: DataResponse<Any>?
// When
let request = session.request("https://httpbin.org/image/jpeg", interceptor: handler)
.validate()
.responseJSON { response in
json1Response = response
json1Expectation.fulfill()
}
.responseJSON { response in
json2Response = response
json2Expectation.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 2)
XCTAssertEqual(handler.retryCalledCount, 3)
XCTAssertEqual(handler.retryCount, 3)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(json1Response?.result.isSuccess, false)
XCTAssertEqual(json2Response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
let errors: [AFError] = [json1Response, json2Response].compactMap { $0?.error?.asAFError }
XCTAssertEqual(errors.count, 2)
for error in errors {
XCTAssertTrue(error.isResponseSerializationError)
XCTAssertTrue(error.localizedDescription.starts(with: "JSON could not be serialized"))
}
}
func testThatSessionCallsResponseSerializerCompletionsWhenAdapterThrowsErrorDuringRetry() {
// Four retries should occur given this scenario:
// 1) Retrier is called from first response serializer failure (trips retry)
// 2) Retrier is called by Session for adapt error thrown
// 3) Retrier is called again from first response serializer failure
// 4) Retrier is called from second response serializer failure
// Given
let handler = RequestHandler()
handler.throwsErrorOnSecondAdapt = true
let session = Session()
let json1Expectation = self.expectation(description: "request should eventually fail")
var json1Response: DataResponse<Any>?
let json2Expectation = self.expectation(description: "request should eventually fail")
var json2Response: DataResponse<Any>?
// When
let request = session.request("https://httpbin.org/image/jpeg", interceptor: handler)
.validate()
.responseJSON { response in
json1Response = response
json1Expectation.fulfill()
}
.responseJSON { response in
json2Response = response
json2Expectation.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCalledCount, 4)
XCTAssertEqual(handler.retryCount, 4)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(json1Response?.result.isSuccess, false)
XCTAssertEqual(json2Response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
let errors: [AFError] = [json1Response, json2Response].compactMap { $0?.error?.asAFError }
XCTAssertEqual(errors.count, 2)
for error in errors {
XCTAssertTrue(error.isRequestAdaptationError)
XCTAssertEqual(error.localizedDescription, "Request adaption failed with error: URL is not valid: /adapt/error/2")
}
}
func testThatSessionCallsResponseSerializerCompletionsWhenAdapterThrowsErrorDuringRetryForDownloads() {
// Four retries should occur given this scenario:
// 1) Retrier is called from first response serializer failure (trips retry)
// 2) Retrier is called by Session for adapt error thrown
// 3) Retrier is called again from first response serializer failure
// 4) Retrier is called from second response serializer failure
// Given
let handler = RequestHandler()
handler.throwsErrorOnSecondAdapt = true
let session = Session()
let json1Expectation = self.expectation(description: "request should eventually fail")
var json1Response: DownloadResponse<Any>?
let json2Expectation = self.expectation(description: "request should eventually fail")
var json2Response: DownloadResponse<Any>?
// When
let request = session.download("https://httpbin.org/image/jpeg", interceptor: handler)
.validate()
.responseJSON { response in
json1Response = response
json1Expectation.fulfill()
}
.responseJSON { response in
json2Response = response
json2Expectation.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
// Then
XCTAssertEqual(handler.adaptCalledCount, 2)
XCTAssertEqual(handler.adaptedCount, 1)
XCTAssertEqual(handler.retryCalledCount, 4)
XCTAssertEqual(handler.retryCount, 4)
XCTAssertEqual(request.retryCount, 1)
XCTAssertEqual(json1Response?.result.isSuccess, false)
XCTAssertEqual(json2Response?.result.isSuccess, false)
XCTAssertTrue(session.requestTaskMap.isEmpty)
let errors: [AFError] = [json1Response, json2Response].compactMap { $0?.error?.asAFError }
XCTAssertEqual(errors.count, 2)
for error in errors {
XCTAssertTrue(error.isRequestAdaptationError)
XCTAssertEqual(error.localizedDescription, "Request adaption failed with error: URL is not valid: /adapt/error/2")
}
}
// MARK: Tests - Session Invalidation
func testThatSessionIsInvalidatedAndAllRequestsCompleteWhenSessionIsDeinitialized() {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册