NSURLSession 3xx redirects and completion handlers - ios

I have a dataTask + completionHandler approach to downloading data from a web server. So far I have implemented this:
let task = session.dataTaskWithURL(url, completionHandler: {
(pageData,response,error) in
...
...
let code = urlHttpResponse.statusCode
switch code {
case 200:
self.fetchedPages.updateValue(pageData, forKey: pageNumber)
case 404:
self.fetchedPages.updateValue(nil, forKey: pageNumber) //No data exists for that page
default:
self.fetchedPages.updateValue(nil, forKey: pageNumber) //No gurantee data exists for that page
}
NSNotificationCenter.defaultCenter().postNotificationName("pageDataDownloaded", object: self, userInfo: ["numberForDownloadedPage":pageNumber])
What I'm wondering is what happens if statusCode is a 3xx error? Will pageData contain the data at the redirected location? In other words, should I add
case _ where code >= 300 && code < 400:
self.fetchedPages.updateValue(pageData, forKey: pageNumber)
Or will the handler get called again with pageData containing the value at the redirected location and a fresh 200 status code? Or is handling redirects properly something I can only do using a delegate?

If you don't have a delegate or the delegate doesn't implement URLSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:), HTTP redirects will be automatically followed. In that case, you won't see the 30x statuses in your handler.

Related

HTTP DELETE Works From Browser But Not From Postman or IOS App

When attempting an http request to my rest api, I continually get a 401 error when using the following code. I don not get this error making any other type of request. I have provided the function that makes the request below.
func deleteEvent(id: Int){
eventUrl.append(String(id))
let request = NSMutableURLRequest(url: NSURL(string: eventUrl)! as URL)
request.httpMethod = "DELETE"
print(eventUrl)
eventUrl.removeLast()
print(self.token!)
request.allHTTPHeaderFields = ["Authorization": "Token \(self.token)"]
let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
if error != nil {
print("error=\(String(describing: error))")
//put variable that triggers error try again view here
return
}
print("response = \(String(describing: response))")
}
task.resume()
}
When sending the delete request with postman, the rest api just returns the data I want to delete but does not delete it. For reference I have posted the view and permissions classes associated with this request Any help understanding why this may be resulting in an error is greatly appreciated!
Views.py
class UserProfileFeedViewSet(viewsets.ModelViewSet):
"""Handles creating, reading and updating profile feed items"""
authentication_classes = (TokenAuthentication,)
serializer_class = serializers.ProfileFeedItemSerializer
queryset = models.ProfileFeedItem.objects.all()
permission_classes = (permissions.UpdateOwnStatus, IsAuthenticated)
def perform_create(self, serializer):
"""Sets the user profile to the logged in user"""
#
serializer.save(user_profile=self.request.user)
Permissions.py
class UpdateOwnStatus(permissions.BasePermission):
"""Allow users to update their own status"""
def has_object_permission(self, request, view, obj):
"""Check the user is trying to update their own status"""
if request.method in permissions.SAFE_METHODS:
return True
return obj.user_profile.id == request.user.id
HEADER SENT WITH DELETE REQUEST VIA POSTMAN
Preface: You leave out too much relevant information from the question for it to be properly answered. Your Swift code looks, and please don't be offended, a bit beginner-ish or as if it had been migrated from Objective-C without much experience.
I don't know why POSTMAN fails, but I see some red flags in the Swift code you might want to look into to figure out why your iOS app fails.
I first noticed that eventUrl seems to be a String property of the type that contains the deleteEvent function. You mutate it by appending the event id, construct a URL from it (weirdly, see below), then mutate it back again. While this in itself is not necessarily wrong, it might open the doors for racing conditions depending how your app works overall.
More importantly: Does your eventUrl end in a "/"? I assume your DELETE endpoint is of the form https://somedomain.com/some/path/<id>, right? Now if eventUrl just contains https://somedomain.com/some/path your code constructs https://somedomain.com/some/path<id>. The last dash is missing, which definitely throws your backend off (how I cannot say, as that depends how the path is resolved in your server app).
It's hard to say what else is going from from the iOS app, but other than this potential pitfall I'd really recommend using proper Swift types where possible. Here's a cleaned up version of your method, hopefully that helps you a bit when debugging:
func deleteEvent(id: Int) {
guard let baseUrl = URL(string: eventUrl), let token = token else {
// add more error handling code here and/or put a breakpoint here to inspect
print("Could not create proper eventUrl or token is nil!")
return
}
let deletionUrl = baseUrl.appendingPathComponent("\(id)")
print("Deletion URL with appended id: \(deletionUrl.absoluteString)")
var request = URLRequest(url: deletionUrl)
request.httpMethod = "DELETE"
print(token) // ensure this is correct
request.allHTTPHeaderFields = ["Authorization": "Token \(token)"]
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Encountered network error: \(error)")
return
}
if let httpResponse = response as? HTTPURLResponse {
// this is basically also debugging code
print("Endpoint responded with status: \(httpResponse.statusCode)")
print(" with headers:\n\(httpResponse.allHeaderFields)")
}
// Debug output of the data:
if let data = data {
let payloadAsSimpleString = String(data: data, encoding: .utf8) ?? "(can't parse payload)"
print("Response contains payload\n\(payloadAsSimpleString)")
}
}
task.resume()
}
This is obviously still limited in terms of error handling, etc., but a little more swifty and contains more console output that will hopefully be helpful.
The last important thing is that you have to ensure iOS does not simply block your request due to Apple Transport Security: Make sure your plist has the expected entries if needed (see also here for a quick intro).

How to cancel multiple networking requests using Moya

I am currently using Moya to structure my networking calls. Per their docs, I have configured it as the following:
enum SomeAPIService {
case endPoint1(with: Object)
case endPoint2(duration: Int)
}
When calling an endpoint (in this case, endPoint1), I do the following:
let provider = MoyaProvider<SomeAPIService>()
provider.request(.endPoint1(object: object)) { (result) in
switch result {
case let .success(moyaResponse):
finished(Result.success(value: moyaResponse.value))
case let .failure(error):
let backendError = BackendError(localizedTitle: "", localizedDescription: "Some error", code: moyaResponse.statusCode)
finished(Result.failure(error: backendError))
}
})
My goal is, upon the user performing an action, cancel all the networking requests that's happening.
Accordingly, Moya does allow one to cancel requests from the discussion here. From the most upvoted comment, someone mentioned let request_1 = MoyaRequestXXXXX and then ruest_1.cancel()
My problem is:
How would I keep pointer to the requests?
provider doesn't have a cancel() function - so how should I be calling it?
Any help is much appreciated.
Edit:
Per the helpful suggestion about using [Cancellable], I did the following:
(1) In my app's singleton instance called Operator, I added var requests = [Cancellable]()
(2) Every API call is added to the requests array as a Cancellable, like so:
let provider = MoyaProvider<SomeAPIService>()
Operator.shared.requests.append(provider as! Cancellable) //causing error
provider.request(.endPoint1(object: object)) { (result) in
//rest of the block omitted
I think I am not getting the syntax correct, and am adding the provider and not the request. However, since the request is itself a block, where would be the place to add the request?
The request method returns a Cancellable. From the documentation we can read:
The request() method returns a Cancellable, which has only one public function, cancel(), which you can use to cancel the request.
So according to this, I made a simple test and call:
var requests: [Cancellable] = []
#objc func doRequests() {
for i in 1...20 {
let request = provider.request(MyApi.someMethod) {
result in
print(result)
}
requests.append(request)
}
requests.forEach { cancellable in cancellable.cancel() } // here I go through the array and cancell each request.
requests.removeAll()
}
I set up a proxy using Charles and it seems to be working as expected. No request was sent - each request was cancelled.
So, the answer to your questions is:
You can keep it in [Cancellable] array.
Go through the array and cancel each request that you want to cancel.
EDIT
The main problem is that you adding the provider to the array and you try to map provider as Cancellable, so that cause the error.
You should add reqest to the array. Below you can see the implementation.
let provider = MoyaProvider<SomeAPIService>()
let request = provider.request(.endPoint1(object: object)) { // block body }
Operator.shared.requests.append(request)
//Then you can cancell your all requests.
I would just cancel the current provider session + tasks:
provider.manager.session.invalidateAndCancel()

Alamofire queue request if previous query is executing

When I send first request and then again I send a second request, Alamofire cancels the first request and gives error as -999 Canceled on it.
What is the way to solve so that both request get processed?
So what I want to do is, user clicks on a button one request is sent, and before that request has completed user clicks on the button again. This behaviour cancels the earlier request, rather what I want it do is, when the button is clicked and the request is sent, it should check whether the previous request is completed or still running. If it is completed then the request should process normally but if the previous request is not completed it should wait for the request to complete and once that is completed this request should be sent
self.alamoFireManager.request(request )
.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
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on page")
print(response.result.error!)
print("Failure")
if( response.result.error?.code == -999 )
{
print("Previous Query is still Executing")
return
}

Authenticated http request swift Alamofire

I'm struggling with getting this to work to make request to my API. Without a token works, but when I try to add additional headers, things turn to be complicated, for me.
First, the structure.
one class called: APIAsyncTask that makes the requests
one class called APIParams, just a data holder to send parameters to the APIAsyncTask class.
one class called DatabaseAPI that makes that builds the parameters, and send that to the APIAsyncTask class.
DatabaseAPI
func someMethod()
{
let task = APIAsyncTasks()
task.registerCallback { (error, result) -> Void in
print("Finished task, back at DatabaseAPI")
}
let params2 = APIParams(request: .GET, apiPath: "Posts/1", apiToken: "4iTX-56w")
task.APIrequest(params2)
}
APIAsyncTask
This part is for fixing another error, because manager was not global, the task got cancelled quickly.
var manager : Manager!
init(authenticatedRequest : Bool, token: String?)
{
manager = Alamofire.Manager()
print("Pre \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
if(authenticatedRequest && token != nil)
{
var defaultHeaders = Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders!
defaultHeaders["Authorization"] = "bearer \(token)"
let configuration = Manager.sharedInstance.session.configuration
configuration.HTTPAdditionalHeaders = defaultHeaders
manager = Alamofire.Manager(configuration: configuration)
}
print("Post \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
}
After some decision making, it comes down to this part.
private func GetRequest(url: String!,token : String?, completionHandler: (JSON?, NSURLRequest?, NSHTTPURLResponse?, NSError?) -> () ) -> ()
{
print("Begin Get Request")
if(token != nil)//if token is not nil, make authenticated request
{
print("just before request: \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
manager.request(.GET, url, parameters: nil, encoding: .JSON).responseJSON { (request, response, json, error) in
print("Get Request (authenticated), inside alamofire request")
var resultJson : JSON?
if(json != nil)
{
resultJson = JSON(json!)
}
completionHandler(resultJson, request, response, error)
}
}
else
{
//working part without token
So as the code is now, I get an error on completing:
Mattt himself gives the answer of using Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders
, so that should be fine...
I suspect it has something to do with the multiple threads, according to this blog. Or, since it is something about CFNetwork, it could be because my API does not use SSL? I disabled NSAppTransportSecurity
I'm kind of new to swift, so examples would be really appreciated! Thankyou!
So the majority of your code looks solid.
The error leads me to believe that CFNetwork is having difficulty figuring out how to compute the protection space for the challenge. I would also assume you are getting a basic auth challenge since you are attaching an Authorization header.
Digging through your logic a bit more with this in mind led me to see that your not attaching your token to the string properly inside the Authorization header. You need to do the following instead.
defaultHeaders["Authorization"] = "bearer \(token!)"
Otherwise your Authorization header value is going to include Optional(value) instead of just value.
That's the only issue I can see at the moment. If you could give that a try and comment back that would be great. I'll update my answer accordingly if that doesn't actually solve your problem.
Best of luck!
You can add your headers in your request with Alamofire 2 and Swift 2.
For an example: go to example

save session in http request swift

in my app I'm using JSON and I made a session recently so if I would like to make some http request to get data for a specific user, the user must log in before (also used by http request).
in the safari when I entering the url's of login and then the url of receive data, it does that as needed.
but in my app, I first call login and then the url for getting data, but it's probably starting a new session in every url request which leads me to get an error and not receive the data.
my url request function is:
static func urlRequest (adress: String, sessionEnded: (NSDictionary->Void)?){
println(adress)
var urli = NSURL(string: adress)
var request = NSURLRequest(URL: urli!)
var rVal = "";
self.task = NSURLSession.sharedSession().dataTaskWithURL(urli!) {(data, response, error) in
var parseError: NSError?
let parsedObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.AllowFragments,
error:&parseError)
let po = parsedObject as NSDictionary
if let a = sessionEnded{
sessionEnded!(po)
}
}
task!.resume()
}
thanks in advance!!
You have shared only half of the puzzle with us, the client code. We can't comment on why the app isn't working with a clearer picture of what the server API. For example, once you "log in", how do subsequent queries confirm that the request is coming from valid session. Furthermore, you report that "every url request which leads me to get an error". Well, what error do you receive? You have to be far more specific regarding the precise errors/crashes you are receiving. BTW, are you logging on to some service with a well-defined API or are you writing that code yourself, too?
Having said that, I might suggest a few refinements to this method:
The sessionEnded (which I've renamed completionHandler to conform to informal standard naming conventions), probably should return an optional NSError object, too, so the caller can detect if there was an error.
Your unwrapping of the sessionEnded completion handler can be simplified to use ?.
When you parse the object, you should feel free to perform the optional cast, too.
You probably want to detect a network error (in which case data would be nil) and return the network NSError object.
Minor point, but I'd probably also rename the function to conform to Cocoa naming conventions, using a verb to start the name. Perhaps something like performURLRequest.
This is your call, but I'd be inclined to have the method return the NSURLSessionTask, so that the caller could use that task object if it wanted to (e.g. save the task object so that it could cancel it later if it wanted to).
Thus, that yields something like:
func performURLRequest (address: String, completionHandler: ((NSDictionary!, NSError!) -> Void)?) -> NSURLSessionTask {
let url = NSURL(string: address)
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data, response, error) in
if data == nil {
sessionEnded?(nil, error)
} else {
var parseError: NSError?
let parsedObject = NSJSONSerialization.JSONObjectWithData(data, options: nil, error:&parseError) as? NSDictionary
completionHandler?(parsedObject, parseError)
}
}
task.resume()
return task
}
And you'd invoke it like:
performURLRequest("http://www.example.com/some/path") { responseDictionary, error in
if responseDictionary == nil {
// handle error, e.g.
println(error)
return
}
// use `responseDictionary` here
}

Resources