Alamofire + Combine: Get the HTTP response status code - ios

I am currently using Alamofire which contains Combine support and using it following way:
let request = AF.request(endpoint)
...
request
.publishDecodable(type: T.self, decoder: decoder)
.value()
.eraseToAnyPublisher()
This will publish result and AFError but from subscriber's .sink, I can't find anywhere to get the HTTP status code. What's the best way to get the status code in subscriber?

If you want the response code, don't erase the DataPublisher using .value(). Instead, use the DataResponse you get from the various publish methods, which includes all of the various response information, including status code. You can then .map it into whatever type you need.

For Swift 5.X and Xcode 12.4
For debugging purposes you can intercept the response right before the Combine publisher (publishDecodable()) and get some of the elements of the URL Response, with :
session.request(signedRequest)
.responseJSON { response in
print(response.request) // original URL request
print(response.response) // URL response
print(response.data) // server data
print(response.result) // result of response serialization
}

The easy MVVM way:
func fetchChats() -> AnyPublisher<ChatListModel, AFError> {
let url = URL(string: "Your_URL")!
AF.request(url, method: .get)
.validate()
.publishDecodable(type: ChatListModel.self)
.value()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Later in viewModel
private var subscriptions: Set<AnyCancellable> = []
// some func
dataManager.fetchChats()
.sink {[weak self] completion in
guard let self = self else { return }
switch completion {
case .failure(let error):
switch error.responseCode {
case 401:
//do something with code
default:
print(error.responseCode)
}
print("All errors:\(error)")
case .finished:
break
}
} receiveValue: {[weak self] message in
guard let self = self else { return }
self.message = message
}
.store(in: &subscriptions)

Related

How to merge two publishers with Combine?

I have an token publisher. It returns a token value as string. I want to make a request using this token in request publisher. I have no idea how to do it. Maybe these codes will help.
Token Transactions:
// Token Publisher
func getAccessToken() -> AnyPublisher<String, Error> {
let url = "it doesn't matter"
var urlRequest = URLRequest(url: url)
// some url request setups
// ...
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.map(\.data)
.decode(type: AccessToken.self, decoder: JSONDecoder())
.map({ $0.token ?? "" })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Generic Request Transactions:
// Request Publisher (The T is decodable generic type.)
func request<T: Decodable>(ofType: T.Type, apiURL: APIURL, method: HTTPMethods) -> AnyPublisher<T, Error> {
// This flatMap is not invoked. :/
AuthManager.shared.getAccessToken()
.flatMap({ accessToken -> AnyPublisher<T, Error> in
guard let url = URL(string: apiURL.url) else {
return Fail(error: NSError(domain: "Missing API URL", code: -10001, userInfo: nil)).eraseToAnyPublisher()
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
urlRequest.setValue("Authorization", forHTTPHeaderField: "Bearer " + accessToken)
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}).eraseToAnyPublisher()
}
Example Request Transactions:
// Example Request
private var cancellables = Set<AnyCancellable>()
func getExampleRequest(id: String) {
let url = "it doesn't matter"
APIManager.shared.request(ofType: ExampleModel.self, apiURL: url, method: .get).sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { exampleModelData in
// print(exampleModelData)
}.store(in: &cancellables)
}
Thanks in advance.
What you have here is a runtime problem that isn't reproducible given the code you presented. I can't give you an answer but I can help you find the problem.
The .print() operator is your friend. put it just before the flatMap and you should see that you aren't getting a next event, but you will likely see that you are getting a canceled or finished event.
If you are getting a finished event, move the print operator up the publisher chain until you figure out where the problem is. If you are getting a cancelled event, check to see why your cancellable is being deinted before completion.

Alamofire publish decodable

I have a HTTP request to my server like this.
func loginUser( username: String, password: String )->AnyPublisher<UserModel, Error>{
let url = URL( string: "\(Credentials.BASE_URL)auth/login")!
let userSignInModel = SignInModel(username: username, password: password)
return AF.request(url,
method: .post,
parameters: userSignInModel,
encoder: JSONParameterEncoder.default)
.validate()
.publishDecodable(type: UserModel.self)
.value()
.mapError{$0 as Error}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
and get response like this
self.dataManager.loginUser(username: self.logInUsername, password: self.logInPassword)
.sink { (response) in
print( response )
switch response {
case .failure( let error ):
self.createAlert(with: error.localizedDescription,
for: .loginAlert,
responseCode: error.asAFError?.responseCode)
case .finished:
break
}
} receiveValue: { (userModel) in
self.token = userModel.token
self.userID = userModel.user.id
}.store(in: &cancellableSet)
but the problem is that I am not able to get error message from the server, how it can be done?
There are several different approaches to parsing responses which return success or response values. Perhaps the simplest is to map any initial failures to your own error type which parses the information you need. For example, given this error type:
struct NetworkError: Error {
let initialError: AFError
let backendError: BackendError?
}
Where BackendError encapsulates the information returned from the backend. Then you can map the response from Alamofire.
AF.request(url,
method: .post,
parameters: userSignInModel)
.validate()
.publishDecodable(type: UserModel.self)
.map { response in
response.mapError { error in
// Check to see if it's an error that should have backend info.
// Assuming it is, parse the BackendError.
let backendError = response.data.flatMap { try? JSONDecoder().decode(BackendError.self, from: $0) }
return NetworkError(initialError: error, backendError: backendError)
}
}
Other alternatives include an enum-based response type that separates your success and failures, or your own response serializer which does the error parsing internally.

refresh token using combine networking ios

this question is straight forward :
My code :
return urlSession.dataTaskPublisher(for: urlRequest)
.tryMap { (data: Data, response: URLResponse) -> Data in
//TODO: hide loader
GRP.hideLoader()
if let httpURLResponse = response as? HTTPURLResponse {
if !(200...299 ~= httpURLResponse.statusCode) {
var error = NetworkingError(errorCode: httpURLResponse.statusCode)
if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
error.jsonPayload = json
}
throw error
}
}
if withErrorMessage, let errorCheckModel = try? JSONDecoder().decode(ErrorModel.self, from: data)
{
if let statusIsSuccess = errorCheckModel.success, let errorMessage = errorCheckModel.message, !errorMessage.isEmpty
{
if(!statusIsSuccess)
{
print(urlString)
GRP.showToast(failure: true, message: errorMessage)
}
}
}
return data
}.mapError { error -> NetworkingError in
return NetworkingError(error: error)
}
.decode(type: T.self, decoder: decoder)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
i made this task buikder but i am stuck, i want to know how can i implement refresh token i. Thank you.
The question is kind of confusing as written. Do you mean I have a request that returns an AnyPublisher<SomeDecodable, NetworkError> and if it fails for a specific reason then I want to make another call (to refresh) and then retry the request? If so it looks something like this:
let authenticatedRequest = URLSession.shared.dataTaskPublisher(for: urlRequest)
return authenticatedRequest
.map { (data, response) -> AnyPublisher<(Data, URLResponse), Error> in
isUnanthenticated(response)
? refetchToken.map { _ in
authenticatedRequest
}
.switchToLatest()
.eraseToAnyPublisher()
: Just((data, response)).eraseToAnyPublisher()
}
.switchToLatest()
.decode(T.self, from: decoder)
.mapError { error -> NetworkingError in
return NetworkingError(error: error)
}
.eraseToAnyPublisher()
}
We make the authenticated request
We map the request and if it failed then we make a reauthrequest and retry. Otherwise we just return out input.
Either way we now have a Publisher of Publishers and we don't want that so we call switch to latest to flatten it and we continue.

Trouble posting API request with Combine

I'm new to the Combine game and am trying to figure out how to generalize a HTTP POST request.
I created the following APIService class to extend individual resource services from:
import Foundation
import Combine
class APIService {
let decoder: JSONDecoder
let session: URLSession
init(session: URLSession = URLSession.shared, decoder: JSONDecoder = JSONDecoder()) {
self.decoder = decoder
self.session = session
}
}
// MARK: JSON API
extension APIService {
struct Response<T> {
let response: HTTPURLResponse
let value: T
}
func post<T: Codable>(
payload: T,
url: URL
) -> AnyPublisher<Response<T>, APIError> {
return Just(payload)
.setFailureType(to: APIError.self) // <<< THIS WAS MISSING!
.encode(encoder: JSONEncoder())
.flatMap({ [weak self] payload -> AnyPublisher<Data, Error> in
guard let self = self else {
return Fail(error: .default("Failing to establish self.")).eraseToAnyPublisher()
}
var request = URLRequest(url: url)
request.httpMethod = Methods.post
request.setValue(
Mimetypes.json,
forHTTPHeaderField: Headers.contentType
)
request.httpBody = payload
return self.session
.dataTaskPublisher(
for: request
)
.tryMap { response -> Response<T> in
let value = try self.decoder.decode(T.self, from: response.data)
return Response(
value: value,
response: response.response
)
}
.mapError { error in
return APIError.default(error.localizedDescription)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
})
.eraseToAnyPublisher()
}
}
However, this class won't compile with the following error at the post function.
Type of expression is ambiguous without more context
Being new to Swift in general and Combine in particular, I am unfortunately out of ideas on how to proceed.
Any help is greatly appreciated!
Figured it out myself: Solution
Add a Failure type to the Just, so input and output Failure types to flatMap are equal. Or put differently: flatMap cannot convert the Never failing Just to a Publisher with Failure.
The missing line in my case:
Just(payload)
.setFailureType(to: APIError.self)
You just have a few compile-time mistakes, which the Swift type inference system isn't able to pinpoint when they happen within a notoriously cranky flatMap Combine operator.
First, you're using the wrong order of parameters and the type of URLResponse, in creating a Response object. Correct it to:
return Response(
response: response.response as! HTTPURLResponse,
value: value
)
Second, your flatMap is not actually returning AnyPublisher<Data, Error> - the return type you specified inside its closure. The return type is AnyPublisher<Response<T>, APIError>. So, you can change that, but then you'll run into another problem, which is that the Error type of flatMap has to be the same as its upstream, which currently is not APIError, so I'd suggest just moving the mapError out of flatMap. It would look like this:
return Just(payload)
.encode(encoder: JSONEncoder())
.flatMap({ [weak self] payload -> AnyPublisher<Response<T>, Error> in
guard let self = self else {
return Fail(error: APIError.default("...")).eraseToAnyPublisher()
}
var request = URLRequest(url: url)
request.httpMethod = "Methods.post"
request.setValue(
Mimetypes.json,
forHTTPHeaderField: Headers.contentType
)
request.httpBody = payload
return self.session
.dataTaskPublisher(
for: request
)
.tryMap { response -> Response<T> in
let value = try self.decoder.decode(T.self, from: response.data)
return Response(
response: response.response as! HTTPURLResponse,
value: value
)
}
.eraseToAnyPublisher()
})
.mapError { error in
return APIError.default(error.localizedDescription)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
Figured it out, thanks to this solid guide to debugging Publishers on another question.
The Just needs to be augmented with a Failure, because flatMap needs the Failure of input and output streams to be the same.
We can use setFailureType(to: <T>) to do so. I have updated my question to reflect this solution.

Input and output type mismatch in Combine data fetching

I am new to reactive programming and Combine, I have the following method which fetching the weather Data from the API.
First, I am checking that if I get 200 if not then throw an error.
If I get the proper data I am decoding it via JSONDecoder but just to
check if there is some problem JSON decoding I am returning the default Object.
Finally mapping the error which is thrown in the
first step but I am getting the following error in flatMAp function
instance method
flatMap(maxPublishers:_:)' requires the types 'Publishers.TryMap.Failure' (aka 'Error') and 'Just.Failure' (aka 'Never') be equivalent
private func fetchDataFor(urlStr: String) -> AnyPublisher<WeatherData, Error> {
let url = URL(string: urlStr)!
return URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ (data, response) in
let response = (response as? HTTPURLResponse)
if response?.statusCode != 200 {
throw NSError(domain: "Error", code: response!.statusCode, userInfo: .none)
}
return data
})
.flatMap{ data in
Just(data)
.decode(type: WeatherData.self, decoder: JSONDecoder())
.catch{ error in
return Just(defaultWeatherData)
}
}
.mapError{ error in
return error
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
Can somebody guide what is wrong here or I am using some wrong approach. Thanks
Change your function to this:
private func fetchDataFor(urlStr: String) -> AnyPublisher<WeatherData, Error> {
let url = URL(string: urlStr)!
return URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ (data, response) in
let response = (response as? HTTPURLResponse)
if response?.statusCode != 200 {
throw NSError(domain: "Error", code: response!.statusCode, userInfo: .none)
}
return data
})
.decode(type: WeatherData.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
Combine has helpful build-in .decode method, where you can convert your data to the local model.
Also, a good practice is not to use .receive(on: RunLoop.main) in the request methods, but let your ViewModel/Interactor (simply a consumer of response) decide on which thread it wants to receive the response.

Resources