Alamofire retry request with modified payload - swift

I have been looking at making use of alamofire to retry a request when I get a certain 400 error and so far the retry works however I am not sure if it is possible to modify the request object so that when retrying it has an updated payload.
Any suggestions and links to reading material are welcome.
Here is my code :
class HTTP412Retrier: RequestRetrier, RequestAdapter {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
print("called") // this is not being printed
return urlRequest
}
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 412 {
completion(request.retryCount < 3, 0.0)
} else {
completion(false, 0.0)
}
}
}

The docs talk about adapting and retrying requests so I assume this would be what you are looking for.
I've written the following and it hits the adapt(_:) function as expected
import UIKit
import Alamofire
class ViewController: UIViewController {
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")
sessionManager.request("http://example.com")
}
}
class AccessTokenAdapter: RequestAdapter {
private let accessToken: String
init(accessToken: String) {
self.accessToken = accessToken
}
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
guard let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://example.com") else { return urlRequest }
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
}

Related

Alamofire retry doesn't work with validate()

I started studying Swift and Alamofire. But I ran into a problem with RequestInterceptor: retry func doesn't work.
My Request Interceptor:
class MyRequestInterceptor: RequestInterceptor {
// MARK: - ADAPT
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
print("-------ADAPT---------")
var request = urlRequest
guard let token = TokenService.getAccess() else {
return
}
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
completion(.success(request))
}
// MARK: - RETRY
func retry(_ request: Request, for session: Session, dueTo error: ApiError, completion: #escaping (RetryResult) -> Void) {
print("--------RETRY-------")
guard let refresh = TokenService.getRefresh() else {
completion(.doNotRetry)
return
}
let parameter = Refresh(token: refresh)
AF.request(ApiManager.refresh.path, method: .post, parameters: parameter)
.responseDecodable(of: TokensPair.self) { response in
guard let tokens = response.value else {
completion(.doNotRetry)
return
}
TokenService.setAccess(token: tokens.access)
TokenService.setRefresh(token: tokens.refresh)
completion(.retry)
}
}
}
My Session Manager:
class SessionManager {
static let shared = SessionManager()
let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(
configuration: configuration,
interceptor: MyRequestInterceptor()
)
}()
}
My request:
func checkUser(complition: #escaping (Result<User, ApiError>) -> Void) {
SessionManager.shared.sessionManager.request(ApiManager.me.path).validate().responseDecodable(of: User.self) {
response in
guard let user = response.value else {
complition(.failure(.invalidCred))
return
}
complition(.success(user))
}
}
Adapt work correctly. But retry never works. What am I doing wrong?
I'm using validate() and expect the status code: 401 to cause an error that will cause a retry, but it doesn't work.
I think problem in retry implementation. Because when i try replace
class MyRequestInterceptor: RequestInterceptor
to
class MyRequestInterceptor: RequestRetrier, RequestAdapter
I get error:
Type 'MyRequestInterceptor' does not conform to protocol 'RequestRetrier'
But i don't know why. And I didn't find solution.
I use:
Xcode 13.2.1,
Swift: 5,
Alamofire: 5.4.4.
Thanks!
You used ApiError in the retry method signature, which breaks the conformance. It works with RequestInterceptor because that type provides a default implementation. Match the proper signature, ensure you have no local types shadowing the types in the signature, and it should work correctly.

Generic parameter 'T' could not be inferred: in Common Webservice method

I am trying to make generic post method for API call.In my loadNew method I want to add normal dictionary inside resource object.Resource contains normal data which will pass from controller class.And dictionary is passed as body of request. but while encoding "Generic parameter 'T' could not be inferred" showing. How do I use dictionary in it?
struct Resource<T> {
let url: URL
let request: URLRequest
let dictionary : [String:Any]
let parse: (Data) -> T?
}
final class Webservice {
// MARK:- Generic
func load<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
URLSession.shared.dataTask(with: resource.url) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
func loadNew<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
do {
//FIXIT: error is getting here
let jsonBody = try JSONEncoder().encode(resource.dictionary)
request.httpBody = jsonBody
}catch{}
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
}
This method is called inside my Login controller.I have also tried assign it directly to request object but same error is showing
func APICall(){
guard let url = URL(string: Constants.HostName.local + Constants.API.User_Login) else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let resources = Resource<LoginReponse>(url: url, request: request, dictionary: dict){
data in
let loginModel = try? JSONDecoder().decode(LoginReponse.self, from: data)
return loginModel
}
// var response = LoginReponse()
Webservice().loadNew(resource: resources) {
result in
if let model = result {
print(model)
}
}
}
The error is a bit misleading, and may indicate you're using an older version of Xcode. In 11.4.1, the error is much more explicit:
error: value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols
The problem is that [String: Any] is not Encodable, because there's no way to encode "Any" (what should happen if you passed a UIViewController here? Or a CBPeripheral?)
Instead of a dictionary here, looking at your code I would expect you to pass an encodable object here. For example:
struct Resource<Value: Decodable, Parameters: Encodable> {
let url: URL
let request: URLRequest
let parameters : Parameters?
let parse: (Data) -> Value?
}
final class Webservice {
func loadNew<Value, Parameters>(resource: Resource<Value, Parameters>, completion: #escaping (Value?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if let parameters = resource.parameters {
request.httpBody = try? JSONEncoder().encode(parameters)
}
// ...
}
That said, I'd probably turn this system around a bit. If you want to have a Request<T> (parameterized on the thing it returns, and not on the parameters it takes to generate it), that's fine. You can pack a bit more into the struct. For example:
let baseURL = URL(string: "https://example.com/api/")!
struct Resource<Value> {
let urlRequest: URLRequest
let parse: (Data) -> Result<Value, Error>
// Things you want as default for every request
static func makeStandardURLRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
return request
}
}
// It would be nice to have a default parser when you can, but you don't have to put that
// into Webservice. The Resource can handle it.
extension Resource where Value: Decodable {
init(urlRequest: URLRequest) {
self.init(urlRequest: urlRequest, parse: { data in
Result { try JSONDecoder().decode(Value.self, from: data) }
})
}
}
And then Resources are smart about themselves:
struct LoginParameters: Encodable {
let username: String
let password: String
}
struct LoginResult: Decodable {
let authToken: String
}
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent("login"))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
Of course that may get repeated a lot, so you can hoist it out:
extension Resource where Value: Decodable {
static func makeStandardURLRequest<Parameters>(endpoint: String, parameters: Parameters) -> URLRequest
where Parameters: Encodable {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent(endpoint))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
And then Login looks like:
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
return makeStandardURLRequest(endpoint: "login", parameters: parameters)
}
}
The point is that you can pull duplicated code into extensions; you don't need to stick it in the Webservice, or add more generic.
With that, your load gets a bit simpler and much more flexible. It focuses just on the networking part. That means that it's easier to swap out with something else (like something for unit tests) without having to mock out a bunch of functionality.
func load<Value>(request: Resource<Value>, completion: #escaping (Result<Value, Error>) -> ()) {
let session = URLSession.shared
session.dataTask(with: request.urlRequest) { data, response, error in
DispatchQueue.main.async {
if let data = data {
//completion call should happen in main thread
completion(request.parse(data))
} else if let error = error {
completion(.failure(error))
} else {
fatalError("This really should be impossible, but you can construct an 'unexpected error' here.")
}
}
}.resume()
}
There's a lots of ways to do this; for another, see this AltConf talk.

How to mock DataTaskPublisher?

I'm trying to write some unit tests for my API using URLSession.DataTaskPublisher. I've found an already existing question on Stackoverflow for the same but I'm struggling to implement a working class using the proposed solution.
Here's the existing question: How to mock URLSession.DataTaskPublisher
protocol APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher
}
class APISessionDataTaskPublisher: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
}
var session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
}
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// How can I return a mocked URLSession.DataTaskPublisher here?
}
}
My API then uses the above like this:
class MyAPI {
/// Shared URL session
private let urlSession: APIDataTaskPublisher
init(urlSession: APIDataTaskPublisher = APISessionDataTaskPublisher(session: URLSession.shared)) {
self.urlSession = urlSession
}
}
What I don't know is how to implement URLSessionMock.dataTaskPublisher().
It would probably be simpler not to mock DataTaskPublisher. Do you really care if the publisher is a DataTaskPublisher? Probably not. What you probably care about is getting the same Output and Failure types as DataTaskPublisher. So change your API to only specify that:
protocol APIProvider {
typealias APIResponse = URLSession.DataTaskPublisher.Output
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError>
}
Conform URLSession to it for production use:
extension URLSession: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
return dataTaskPublisher(for: request).eraseToAnyPublisher()
}
}
And then your mock can create the publisher in any way that's convenient. For example:
struct MockAPIProvider: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!
let data = "Hello, world!".data(using: .utf8)!
return Just((data: data, response: response))
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
}
}
If you store in UT bundle stub JSON (XML, or something) for every API call that you want to test then the simplest mocking code might look as following
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// here might be created a map of API URLs to cached stub replies
let stubReply = request.url?.lastPathComponent ?? "stub_error"
return URLSession.shared.dataTaskPublisher(for: Bundle(for: type(of: self)).url(forResource: stubReply, withExtension: "json")!)
}
}
so instead call to network server your publisher is created with URL of locally stored resource with known data, so you can verify all your workflow.
I will develop the step from having a simple Get request, to mocking .dataTaskPublisher for Combine and for the last part, testing the call. It is a ready to use code, for everyone in case someone else would need it.
Follow the comment to add your model or anything that depends on your project data.
So this is the protocol that give the rules to my NetworkRequest class:
protocol NetworkRequestProtocol {
associatedtype Resource
var resourceURL: NetworkEndpoint { get set }
var resourceSession: URLSession { get set }
func download() -> AnyPublisher<Resource, NetworkError>
}
There is some custom class, NetworkEndpoint and NetworkError, you can add your own here if you want or use URL and URLError instead:
enum NetworkEndpoint {
static let baseURL = URL(string: "API_BASE_URL")! // Add your api base url here
case live
var url: URL {
switch self {
case .live:
return NetworkEndpoint.baseURL!.appendingPathComponent("END_OR_YOUR_API_URL") // Add the end of your API url here
}
}
}
enum NetworkError: LocalizedError {
case addressUnreachable(URL)
case invalidResponse
var errorDescription: String? {
switch self {
case .invalidResponse:
return "The server response is invalid."
case .addressUnreachable(let url):
return "\(url.absoluteString) is unreachable."
}
}
}
Now, I am creating the NetworkRequest class to handle the API call. RessourceSession initializer is used for the UnitTest part only:
final class NetworkRequest<Resource> where Resource: Codable {
var resourceURL: NetworkEndpoint
var resourceSession: URLSession
init(_ resourceURL: NetworkEndpoint,
resourceSession: URLSession = URLSession(configuration: .default)) {
self.resourceURL = resourceURL
self.resourceSession = resourceSession
}
// MARK: - Dispatch Queues
let downloadQueue = DispatchQueue(
label: "downloadQueue", qos: .userInitiated,
attributes: .concurrent, autoreleaseFrequency: .inherit, target: .main)
}
// MARK: - Network Requests
extension NetworkRequest: NetworkRequestProtocol {
func download() -> AnyPublisher<Resource, NetworkError> {
resourceSession
.dataTaskPublisher(for: resourceURL.url)
.receive(on: downloadQueue)
.map(\.data)
.decode(type: Resource.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
switch error {
case is URLError:
return .addressUnreachable(self.resourceURL.url)
default:
return .invalidResponse }}
.eraseToAnyPublisher()
}
}
For the production code, this is an example of use of the NetworkRequest class, and of course, your model must be Codable:
var subscriptions = Set<AnyCancellable>()
func downloadData() {
NetworkRequest<YOUR_MODEL_NAME>(.live).download() // Add your model name inside the brackets
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
break }},
receiveValue: { data in
print(data) })
.store(in: &subscriptions)
}
So now that all the code is setup in the project, we can pass to the UnitTest part and start mocking URLSession:
class MockURLSession: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
override func dataTask(with request: URLRequest,
completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return MockURLSessionDataTask {
completionHandler(data, response, error)
}
}
}
Now, we mock URLSessionDataTask that we return when overriding dataTask in MockURLSession, and it will work for .dataTaskPublisher:
class MockURLSessionDataTask: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
We create fake response data to pass into our tests, but you must create a .json file with your data in it to fetch them in the tests:
class FakeResponseData {
static let response200OK = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!
static let responseKO = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 500,
httpVersion: nil,
headerFields: nil)!
class RessourceError: Error {}
static let error = RessourceError()
static var correctData: Data {
let bundle = Bundle(for: FakeResponseData.self)
let fakeJsonURL = bundle.url(forResource: "FAKE_JSON_FILE_NAME", withExtension: "json") // Add your fake json file name in here
let fakeJsonData = try! Data(contentsOf: fakeJsonURL!)
return fakeJsonData
}
static let incorrectData = "error".data(using: .utf8)!
}
And to finish, this is the part where you test your NetworkRequest, with the fake data coming from the .json file, or the error. You use resourceSession initializer to add your MockURLSession here and avoid making real network call:
class NetworkRequestTests: XCTestCase {
var expectation: XCTestExpectation!
var subscriptions: Set<AnyCancellable>!
override func setUpWithError() throws {
try super.setUpWithError()
expectation = XCTestExpectation(description: "wait for queue change")
subscriptions = Set<AnyCancellable>()
}
override func tearDownWithError() throws {
subscriptions = nil
expectation = nil
try super.tearDownWithError()
}
func testNetworkRequest_mockURLSessionAddCorrectDataResponse_returnRatesDataModelValues() throws {
let expectedTestValue = "test" // This value is set in your .json fake data for testing
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: FakeResponseData.correctData,
response: FakeResponseData.response200OK,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertEqual(expectedTimestamp, value.InFakeJson) // Compare with your fake json file
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
func testNetworkRequest_mockURLSessionAddServerErrorAsResponse_returnNetworkErrorInvalidResponse() throws {
let expectedNetworkError = NetworkError.invalidResponse.localizedDescription
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: nil,
response: FakeResponseData.responseKO,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTAssertEqual(expectedNetworkError, error.localizedDescription)
case .finished:
break
}
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertNil(value)
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
}
Answered on original question, but will repost here:
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).

Alamofire 5, How to stop all requests, refresh access token and rerun all stopped requests?

Situation I need to fix - Imagine 2 requests are made in same time (when access token is not valid). Both try to refresh token but one of them will invalidate token for another one.
Is there any way how to:
allow only 1 to refresh token
stop all other requests
rerun all stopped requests (when token is refreshed)
Or do you have any idea how to solve this by other way?
This is how my request look like in every view controller:
AF.request(encodedURLRequest, interceptor: AuthInterceptor()).validate().responseData { (response) in
...
}
This is my AuthInterceptor:
final class AuthInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var adaptedUrlRequest = urlRequest
adaptedUrlRequest.setValue("Bearer \(UserDefaults.standard.getAccessToken())", forHTTPHeaderField: "Authorization")
completion(.success(adaptedUrlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
print("request \(request) failed")
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 403 {
guard let url = URL(string: Endpoint.login.url) else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let parameters: [String: String] = [
"refresh_token": UserDefaults.standard.getRefreshToken(),
"grant_type": "refresh_token"
]
guard let encodedURLRequest = try? URLEncodedFormParameterEncoder.default.encode(parameters,
into: urlRequest) else { return }
AF.request(encodedURLRequest).validate().responseData { (response) in
if let data = response.data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let loginResponse = try? decoder.decode(LoginResponse.self, from: data) {
UserDefaults.standard.setAccessToken(value: loginResponse.accessToken)
UserDefaults.standard.setRefreshToken(value: loginResponse.refreshToken)
completion(.retryWithDelay(1))
}
}
}
}
}
}
You can use Alamofire's RequestRetrier protocol (as part of the RequestInterceptor protocol) to do this without having to manually start and stop requests.
Essentially, you need to know when a refresh is being performed and store any additional completion handlers from requests which failed because of the expired token. You can do this in your AuthInterceptor class. Just make sure your implementation is thread safe!

Alamofire request cancelled (-999)

I want to make use of the RequestAdapter and RequestRetrier protocols, so I created my own so called AuthenticationHandler class which implements both protocols. I do this because the refresh token may be expired so this mechanism comes in handy.
The RequestAdapter protocol method adapt does get called, but the should RequestRetrier protocol method does not. I have a separate class that does the actual request:
class TestRequest {
var authHandler: AuthenticationHandler?
func executeRequest() {
// For testing purposes a false access token is passed
self.authHandler = AuthenticationHandler(accessToken: "some_default_token")
let sessionManager = SessionManager()
sessionManager.adapter = authHandler
sessionManager.retrier = authHandler
var loginModel : LoginMessage = LoginMessage.init()
loginModel.username = "someUserName"
loginModel.password = "WrongPassword"
do {
let binaryData = try loginModel.serializedData()
// Create a file with this binary data in order to use it as part of the multipart formdata
guard let fileURL = createFileFrom(data: binaryData) else {
print("Error creating file")
return
}
// Note: custom headers have been set in the AuthenticationHandler
sessionManager.upload(multipartFormData: { multipartFormData in
multipartFormData.append(fileURL, withName: "content")
},
to: K.endpointLogin) { (encodingResult) in
switch encodingResult{
case .success(let upload, _, _):
upload.responseJSON { response in
print("Encoding result success...")
print("Statuscode: \(response.response?.statusCode)")
print(response)
}
case .failure(let encodingError):
print("Failure: \(encodingError)")
}
}
} catch {
print(error)
}
}
I have followed the example in the documentation here
I have read several previous posts saying it has to do with retaining the sessionManager. But I think that is also covered. My authentication handler looks like this:
class AuthenticationHandler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var accessToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
init(accessToken: String) {
self.accessToken = accessToken
}
// MARK: - RequestAdapter protocol method
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(K.SERVER_URL) {
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
urlRequest.setValue("multipart/form-data", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - RequestRetrier protocol method
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
}
}
My config is as follows:
Alamofire version: 4.7.2
Xcode version: 9.4.1
Swift version: 4
What am I doing wrong here? Why is the request cancelled and is the should method not called?
Any help is greatly appreciated.
Your core issue is that your SessionManager instance is being deinited, which cancels any ongoing tasks. You should keep it around in a singleton or something similar, which will fix the other issue, that of using a new SessionManager for each request, which is an anti pattern.