3

I am performing background uploads to an endpoint that requires authorization using a token. The token is included as an HTTP header to the individual upload tasks that are created/started.

Is it possible to respond to expired token messages and prevent 401s? Roughly, the timeline I am hoping to implement is:

  1. Start upload in foreground with valid token
  2. App is backgrounded, uploads continue but app itself is closed
  3. Some time passes, user is using device with other apps, etc.
  4. Token used in step 1 expires, and all uploads still in progress will require new token.
  5. App is launched in background, telling me that the auth token has expired.
  6. I refresh app token, and uploads are allowed to continue
  7. Uploads report success.

So far I've been unable to get this behavior - at step 5 instead of issuing a challenge/etc. that the session or task delegate can handle, I simply get 401 responses.

Setup config/session/task:

let config = URLSessionConfiguration.background(withIdentifier: "background_upload_session")
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

var request = URLRequest(url: NetworkService.uploadUrl)
request.httpMethod = "POST"
...
request.setValue(authToken, forHTTPHeaderField: "Authorization-Token")
...
let task = session.uploadTask(with: request, fromFile: filePath)
task.taskDescription = filePath.lastPathComponent
task.resume()

Delegate methods:

public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if error != nil {
        DDLogError("NetworkService urlSession didCompleteWithError:\(String(describing: error))")
    }

    if let httpResponse = task.response as? HTTPURLResponse {
        let statusMessage = httpResponse.statusCode == 201 ? "success" : "fail"

        DDLogInfo("NetworkService didCompleteWithError status code: \(httpResponse.statusCode) - \(statusMessage)")
    }
}

public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    DDLogInfo("NetworkService urlSession task didReceive challenge")


    completionHandler(.performDefaultHandling, nil)
}

The didReceiveChallenge method is not called - which is what I was expecting in this case. Assuming it was called, I'm not sure how to respond with a new token in this situation.

Any ideas?

Zach
  • 3,261
  • 5
  • 18
  • 42
  • Does the service you are using have a mechanism to exchange tokens? I've worked on systems that can give you a 401. But there was a defined protocol for obtaining a new token. – Mobile Ben Dec 08 '18 at 00:21
  • I can request a new token via an API call, but that has to be done manually after I encounter an expired token. – Zach Dec 08 '18 at 01:41
  • 1
    Excuse me for my confusion ... where are you having the problem then? Is it the existing calls inflight? If you can manually get the new token, then that is what you should do. Any inflight calls will of course fail because they are using the wrong token. When I worked on a system like this, the calls all had to be able to recover from 401. This was built in to the response handler which was used by every call. – Mobile Ben Dec 08 '18 at 16:38
  • Apologies for being confusing! My intent was to avoid having to re-upload data. Let’s take an example where 1000 uploads are occurring in the background. If the token expiry is low enough, the OS may not complete all uploads before the token expires. But each upload has to complete (either successfully or with failure) before being launched in the background. At that point I’d discover the 401s and get a new token and retry. But that would mean several hundred repeated uploads. Does that make more sense? The background aspect is the tricky part here. – Zach Dec 08 '18 at 16:41
  • And also - one possible solution is to work in smaller batches. If I have to upload 1000 items then I can break it into chunks of 50 let’s say, and then if we get 401s while handling a chunk it’s only 50 at most that get re-uploaded. Not ideal since it requires having the app launched in the background more often, but could work possibly. – Zach Dec 08 '18 at 16:42
  • 1
    It isn't really advisable to dispatch 1000 calls all at once. What you may want to consider is building a queue to dispatch X calls (as you suggested). Then what you can do is assemble your request during pre-flight. This way, these queued calls will get the new token. This is more or less what I did. Does that make sense? Even 50 is a bit high. Note that the OS will probably only handle X calls at once. The rest will "starve" (ie. will be waiting to dispatch) as the OS/works through processing the uploads. – Mobile Ben Dec 08 '18 at 16:46
  • 1
    Another possibility ... does the server tell you the lifetime of the token? I had an expiration date. You could also use that to make decisions on how much to batch. Note this has some downfalls. The server could revoke the token at any time. This could be due to the user cancelling the subscription, the user account was compromised, etc. So this means you can get a bad token *anytime*. Really the best approach is to build a system that works regardless. – Mobile Ben Dec 08 '18 at 16:47
  • Thanks for the input. The two options you lay out both make sense - but have tradeoffs that impact the business ask/desired behavior I was tasked with implementing. Frankly it's surprising that iOS would give us the capability to upload data without being able to respond to 401s like this. Having all subsequent uploads fail because of expired tokens is not a great design - no matter what size batch is used! – Zach Dec 12 '18 at 18:26

0 Answers0