My API usually returns a certain format in JSON (simplified notation):
{
status: // http status
error?: // error handle
data?: // the response data
...
}
In my Combine operators, I take the data from a URLSession dataTaskPublisher and parse the response into a Decodable object that reflects the above schema. That works great.
However, I have an endpoint that returns the HTTP status code 201 (operation successful), and has no data at all. How would I chain this with my operators without throwing an error?
This is what I have:
publisher
.map { (data, response) in
guard data.count > 0 else {
let status = (response as! HTTPURLResponse).statusCode
return Data("{\"status\": \(status), \"data\": \"\"}".utf8)
}
return data
}
.mapError { CustomError.network(description: "\($0)")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { err -> CustomError in CustomError.decoding(description: "\(err)") }
...
As you can see, I simply construct an appropriate response, where the response's "data" is an empty string. However, this is ugly and a bit hacky, and I do not see the reason, why the pipeline should continue with parsing, etc, when I already have all I need. How can I interrupt it and finish the pipeline successfully for its final subscriber?
I would suggest creating a separate Publisher for handling the specific endpoint which doesn't return any Data. You can use a tryMap to check the HTTP status code and throw an error in case it's not in the accepted range. If you don't care about the result, only that there was a successful response, you can map to a Void. If you care about the result (or the status code), you can map to that too.
extension URLSession.DataTaskPublisher {
func emptyBodyResponsePublisher() -> AnyPublisher<Void, CustomError> {
tryMap { _, response in
guard let httpResponse = response as? HTTPURLResponse else { throw CustomError.nonHTTPResponse }
let statusCode = httpResponse.statusCode
guard (200..<300).contains(statusCode) else { throw CustomError.incorrectStatusCode(statusCode) }
return Void()
}.mapError { CustomError.network($0) }
.eraseToAnyPublisher()
}
}
Related
I'm trying to remove my dependency on RxAlamofire.
I currently have this function:
func requestData(_ urlRequest: URLRequestConvertible) -> Observable<(HTTPURLResponse, Data)> {
RxAlamofire.request(urlRequest).responseData()
}
How can I refactor this and use Alamofire directly to build and return an RxSwift Observable?
I suggest you look at the way the library wraps URLRequest to get an idea on how to do it...
Below is an abbreviated example from the library. In essence, you need to use Observable.create, make the network call passing in a closure that knows how to use the observer that create gives you.
Make sure you send a completed when done and make sure the disposable knows how to cancel the request.
Your Base will be something in Alamofire (I don't use Alamofire so I'm not sure what that might be.)
extension Reactive where Base: URLSession {
/**
Observable sequence of responses for URL request.
Performing of request starts after observer is subscribed and not after invoking this method.
**URL requests will be performed per subscribed observer.**
Any error during fetching of the response will cause observed sequence to terminate with error.
- parameter request: URL request.
- returns: Observable sequence of URL responses.
*/
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}
All of my API endpoints return a response which looks something like this in Postman:
{
"statusCode": 401,
"error": "Unauthorized",
"message": "Missing authentication"
}
What I would like to do is make the request, and have access to these properties in Swift. There will be some cases where I use the error message property's value in the front of the app. This will be determined by the statusCode returned.
What I have right now is this:
private var cancellable: AnyCancellable?
let url = URL(string: "http://abc336699.com/create")
self.cancellable = URLSession.shared.dataTaskPublisher(for: url!)
.map { $0.data }
Prior to this, I tried tryMap, but the type of error it returned didn't give me the flexibility I wanted. I then moved on and tried Almofire, but it seemed like an over kill for what I want to do.
I wanted to check what is being returned in the response I get, but I get the following error:
Cannot assign value of type 'Publishers.Map<URLSession.DataTaskPublisher, Data>' to type 'AnyCancellable'
I want simple access to my response errors so I can integrate the API throughout the app using combine.
I am not sure from where you will be getting your data as in JSON response there is no Key for data. Before writing below code my understanding was that you want to check error and statusCode from the mentioned JSON response and then move forward with your business logic. The below code is to give you a vague idea of how we can do that.
enum CustomError: Error {
case custom(_ error: String)
case unknownStatusCode
case errorOccurred
}
let url = URL(string: "http://abc336699.com/create")
func load() -> AnyPublisher<Data,CustomError> {
URLSession.shared.dataTaskPublisher(for: url!)
.map(\.data)
.tryMap { (data) -> Data in
let genericModel = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: AnyObject]
if let statusCode = genericModel?["statusCode"] as? String {
switch statusCode {
case "200":
guard let data = genericModel?["message"] as? Data else {
throw CustomError.custom("Parsing error")
}
return data
default:
if let error = genericModel?["error"] as? String {
throw CustomError.custom(error)
} else {
throw CustomError.unknownError
}
}
}
throw CustomError.errorOccurred
}
.decode(type: YourCustomDecodableModel.self, decoder: JSONDecoder())
.mapError({ $0 as? CustomError ?? CustomError.errorOccurred })
.eraseToAnyPublisher()
}
I am trying to trigger a download when my app becomes active, but I can't figure out how to do it.
This is my code
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).map{ _ in
return self.urlSession.dataTaskPublisher(for: url)
}
.tryMap{ element in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
.....
I have no idea how to get this to work. I constantly get a compile error stating:
Value of type 'URLSession.DataTaskPublisher' has no member 'response'
Why does my return statement return the complete publisher and not its result?
If I remove the NotificationCenter part and call tryMap directly on dataTaskPublisher like this:
self.urlSession.dataTaskPublisher(for: url)
.tryMap{ element in
the compiler does not complain.
Pretty confused here :)
Why does my return statement return the complete publisher and not its result?
It returns the complete publisher because, well, you told it to return the complete publisher. A map's Output is whatever the map transform returns. Your map transform returns a DataTaskPublisher, so the Output of the map is DataTaskPublisher. Therefore the Input to tryMap—the type of the element variable—is a DataTaskPublisher. Roughly speaking, the type after map (before tryMap) is
Publisher<Publisher<(data: Data, response: URLResponse), URLError>, Never>
(This is not a real type because Publisher is not a real type constructor.)
What you want is to flatten that out into
Publisher<(data: Data, response: URLResponse), Error>
You'll need to use setFailureType(to:) to convert the failure type of the notification publisher from Never to Error. And you'll need to use mapError to convert the failure type of the DataTaskPublisher from URLError to Error.
Once you've got the failure types matched up, there are several operators that can flatten nested publishers. The one you're looking for is probably switchToLatest:
func data(for url: URL) -> AnyPublisher<Data, Error> {
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.setFailureType(to: Error.self)
.map({ _ in
self.urlSession.dataTaskPublisher(for: url)
.mapError { $0 as Error }
})
.switchToLatest()
.tryMap({ element in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return element.data
})
.eraseToAnyPublisher()
}
I used Alamofire and PromiseKit as separate Cocoapod installs. I can retrieve the JSON data using Alamofire, but I am receiving the error below when configuring PromiseKit. The error below appears in the line where 'fulfill, reject' are in.
Error message: Contextual closure type '(Resolver<_>) -> Void' expects 1 argument, but 2 were used in closure body
I am using Xcode 9.2 and IOS 11.2 inside of the Simulator. Thank you for your advice in advance!
func wantToReturnAnArrayOfActor() -> Promise<[Actor]> {
return Promise { fulfill, reject in
Alamofire.request(ApiUrl.url.rawValue).responseJSON { (response) in
switch(response.result)
{
case .success(let responseString): print("my response string = \(responseString)")
let actorResponse = ActorApiResponse(JSONString: "\(responseString)")//converts all of the data into the ActorApiResponse model class
return when(fulfilled: actorResponse)
DispatchQueue.main.async {
print("actorResponse = \(String(describing: actorResponse))")
}
case .failure(let error): print("alamofire error = \(error)")
}
}
}
}
Should it rather be like this,
func wantToReturnAnArrayOfActor() -> Promise<[Actor]> {
return Promise() { resolver in
Alamofire.request(ApiUrl.url.rawValue).responseJSON { (response) in
switch(response.result)
{
case .success(let responseObject):
let actorResponse = ActorApiResponse(jsonObject: responseObject)
let actors = actorResponse.getActors()
resolver.fulfill(actors)
case .failure(let error):
resolver.reject(error)
}
}
}
}
The initializer closure for Promise takes in single argument, which is of type Resolver, which is what your error says. Then, you would want to resolve your promise with result which is of type [Actor] when the promise execution is finished or then reject with error if error occurred during the execution.
Few points to note here:
Alamofire.request(_).responseJSON returns json object not json string.
If your ActorApiResponse is the object which transforms the json to [Actor], you should have proper method to convert json object to actual data type ie. [Actor].
You could have your ActorApiResponse something like this,
struct ActorApiResponse {
init(jsonObject: Any) {
}
func getActors() -> [Actor] {
// calculate and return actors
return []
}
}
Then, you can call it from else where,
wantToReturnAnArrayOfActor().done {
// do something with [Actor here]
// You can also chain the multiple promise using .then instead of using done
}.catch { error in
print("Error occurred \(error)")
}
I'm using a lot of async network request (btw any network request in iOS need to by async) and I'm finding way to better handle errors from Apple's dataTaskWithRequest which not supports throws.
I have code like that:
func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ()) {
let request = NSURLRequest(URL: NSURL(string: "http://google.com")!)
if someData == nil {
// throw my custom error
}
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
// here I want to handle Apple's error
}
task.resume()
}
I need to parse my possible custom errors and handle possible connection errors from dataTaskWithRequest. Swift 2 introduced throws, but you can't throw from Apple's closure because they have no throw support and running async.
I see only way to add to my completion block NSError returning, but as I know using NSError is old-style Objective-C way. ErrorType can be used only with throws (afaik).
What's the best and most modern method to handle error when using Apple network closures? There is no way no use throws in any async network functions as I understand?
there are many ways you can solve this, but i would recommend using a completion block which expects a Result Enum. this would probably be the most 'Swift' way.
the result enum has exactly two states, success and error, which a big advantage to the usual two optional return values (data and error) which lead to 4 possible states.
enum Result<T> {
case Success(T)
case Error(String, Int)
}
Using the result enum in a completion block finishes the puzzle.
let InvalidURLCode = 999
let NoDataCode = 998
func getFrom(urlString: String, completion:Result<NSData> -> Void) {
// make sure the URL is valid, if not return custom error
guard let url = NSURL(string: urlString) else { return completion(.Error("Invalid URL", InvalidURLCode)) }
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
// if error returned, extract message and code then pass as Result enum
guard error == nil else { return completion(.Error(error!.localizedDescription, error!.code)) }
// if no data is returned, return custom error
guard let data = data else { return completion(.Error("No data returned", NoDataCode)) }
// return success
completion(.Success(data))
}.resume()
}
because the return value is a enum, you should switch off of it.
getFrom("http://www.google.com") { result in
switch result {
case .Success(let data):
// handle successful data response here
let responseString = String(data:data, encoding: NSASCIIStringEncoding)
print("got data: \(responseString)");
case .Error(let msg, let code):
// handle error here
print("Error [\(code)]: \(msg)")
}
}
another solution would be to pass two completion blocks, one for success and one for error. something along the lines of:
func getFrom(urlString: String, successHandler:NSData -> Void, errorHandler:(String, Int) -> Void)
It's very similar to Casey's answer,
but with Swift 5, now we have Result (generic enumeration) implementation in standard library,
//Don't add this code to your project, this has already been implemented
//in standard library.
public enum Result<Success, Failure: Error> {
case success(Success), failure(Failure)
}
It's very easy to use,
URLSession.shared.dataTask(with: url) { (result: Result<(response: URLResponse, data: Data), Error>) in
switch result {
case let .success(success):
handleResponse(success.response, data: success.data)
case let .error(error):
handleError(error)
}
}
https://developer.apple.com/documentation/swift/result
https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md
There's an elegant approach utilising a JavaScript-like Promise library or a Scala-like "Future and Promise" library.
Using Scala-style futures and promises, it may look as follows:
Your original function
func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ())
may be implemented as shown below. It also shows, how to create a promise, return early with a failed future and how to fulfill/reject a promise:
func sendRequest(someData: MyCustomClass) -> Future<NSData> {
guard let url = ... else {
return Future.failure(MySessionError.InvalidURL) // bail out early with a completed future
}
let request = ... // setup request
let promise = Promise<NSData>()
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard let error = error else {
promise.reject(error) // Client error
}
// The following assertions should be true, unless error != nil
assert(data != nil)
assert(response != nil)
// We expect HTTP protocol:
guard let response = response! as NSHTTPURLResponse else {
promise.reject(MySessionError.ProtocolError) // signal that we expected HTTP.
}
// Check status code:
guard myValidStatusCodeArray.contains(response.statusCode) else {
let message: String? = ... // convert the response data to a string, if any and if possible
promise.reject(MySessionError.InvalidStatusCode(statusCode: response.statusCode, message: message ?? ""))
}
// Check MIME type if given:
if let mimeType = response.MIMEType {
guard myValidMIMETypesArray.contains(mimeType) else {
promise.reject(MySessionError.MIMETypeNotAccepted(mimeType: mimeType))
}
} else {
// If we require a MIMEType - reject the promise.
}
// transform data to some other object if desired, can be done in a later, too.
promise.fulfill(data!)
}.resume()
return promise.future!
}
You might expect a JSON as response - if the request succeeds.
Now, you could use it as follows:
sendRequest(myObject).map { data in
return try NSJSONSerialization.dataWithJSONObject(data, options: [])
}
.map { object in
// the object returned from the step above, unless it failed.
// Now, "process" the object:
...
// You may throw an error if something goes wrong:
if failed {
throw MyError.Failed
}
}
.onFailure { error in
// We reach here IFF an error occurred in any of the
// previous tasks.
// error is of type ErrorType.
print("Error: \(error)")
}