So I have implemented my own NSURLProtocol sub class. I registered it when app launches.
I tried to print all requests in canInitWithRequest:, however while running, it's not getting all the requests, just a few domains.
Is there anything I should notice or be careful? Any possible reasons? What did I miss? Thank in advance.
It kind of depends on how you registered it. You can register it for use with NSURLConnection (and, IIRC, the shared NSURLSession), or you can register it for use with other NSURLSessions individually. If a request happens in a session that your protocol isn't registered in, your protocol won't see it.
Related
I am having an issue with UIWebviews and a couple of custom NSURLProtocols that I have in my app.
All my non web view request are invoked with NSURLSession, so in order for those request to go through the protocols I need to set the setProtocolClasses array on my sessionconfig, at this point everything works as expected.
For my web views, I do a registration on the didFinishLaunchingWithOptions: method in the AppDelegate with the [NSURLProtocol register class[MyProtocol Class]]. If i don't re-register before the execution of the web view request, that web view request won't go through the protocol.
Do you guys have any idea why I have to re-register to my custom protocol every time I try to load a web view request?
What is the request URL? Is it possible that there's another protocol class that gets registered after you? Does canInitWithRequest get called on your class at webview request time?
I have an app working just fine on iOS 9 with a Custom NSURLProtocol implemented with NSURLSession. All the requests executed by the client are done with NSURLSession as well and each sessionConfiguration is registering to the protocol before executing the request.
I have an issue with iOS 8 that I don't have with iOS 9. With iOS8 the Custom NSURLProtocol is performing that request non stop, basically an infinite loop of the same request. canInitWithRequest: in the custom protocol gets called way more on iOS 8 than on iOS 9 which basically drives the method startLoading to get called and fire the request after some header modifications that my protocol is supposed to perform.
Is there a known issue with iOS8 where NSURLProtocols and NSURLSession don't behave as expected?
I'm not aware of any problems like that, no.
This sounds like your protocol is either:
Failing to detect its own header modifications for some reason
Failing to make the modifications for some reason
Failing to return NO when your protocol is asked if it should handle a request that already contains the modified headers
Be sure that you are using a header field for the purpose of detecting whether to handle the request. There's no guarantee that the specific NSURLRequest object that you pass down into the machinery will come back to you. I'm not even sure I would trust propertyForKey:inRequest:, but that might make me too paranoid.
Critically, don't ever subclass NSURLRequest when working with NSURLSession. In iOS 9, it almost works. The farther back you get, the more broken the behavior, until by iOS 7 you're pretty much relegated to using NSURLConnection. :-)
SFSafariViewController is compelling, but has a very simple API that doesn't allow setting any fields on the request it initiates when presented. Are there any clever ways to set the HTTP referer header on requests initiated from SFSafariViewController?
There is no API for that, unfortunately.
You can check SFSafariViewControllerDelegate page - all of the delegate methods is post-load handlers.
WKWebView and WKNavigationDelegate is one of possible replacements (iOS 8.0+).
The only way I could think to do this would be to bounce the request off of a web service that you control.
Sadly you can't use anything other than http or https URLs, so a local file URL won't work (short of listening on a socket on the device).
We use Twilio SDK in our iOS app. It works fine but sometimes didStopListeningForIncomingConnections callback is called with error=31000 ("General error"). After that, the device turns to a strange state: it seems to be online but it's impossible to call it. And it shows "unconnected" state on the device.
So the questions are:
1. What does this 31000 error means?
2. What should we do in such a case? How to reconnect device to Twilio?
Megan from Twilio here.
You can see what an error for Twilio Client means here: https://www.twilio.com/docs/api/client/errors
However, 31000 is a rather vague and less than ideal error message as you describe. In this case, it is likely that the Twilio capability token has probably expired while the application is in the background, and if you merely call the listen method whenever they are receiving the 31000 generic error, it might cause the client SDK to result in a error-retry loop and crash the application eventually.
At the time of your writing with TwilioClient iOS SDK v1.2.5, it is suggested to use the following sample code in your did-stop-listening callback:
- (void)device:(TCDevice*)device didStopListeningForIncomingConnections:(NSError*)error {
if ( [self checkCapabilityTokenStillValid] ) {
// if the token has not yet expired, use the `listen` method of `TCDevice` to resume the listening state
[self.device listen];
}
else {
// restart all over by requesting a new capability token and initialize/update the `TCDevice` instance again
[self login];
}
}
The TwilioClient iOS SDK takes care of dispatching the listen and updateCapabilitiyToken: methods to the current thread for execution, therefore it's safe to call them directly in the didStopListeningForIncomingConnections. The did-stop-listening delegate method is always triggered with dispatch_get_main_queue() of Grand Central Dispatch.
Hope this may help others if they run into the same generic error.
This may or may not be the issue, we have encountered 31000 errors two times in our development and both were a result of generating the JWT on our server api. To be clear the error was a 31000 on the client, but the reason for this was in the construction of the JWT, and the params we wanted twilio to send back to our application.
When passing in an object to allow_client_outgoing or allow_client_incoming the twilio sdk concats this all in their scope attribute in their JWT. It added it to the scope:client:outgoing?appSid= which looks like a query string. That means it has a size limit of 2048. So exceeding this length generates a 31000 error.
In addition adding the objects doesn't seem to always implicitly serialize the object correctly, it introduces characters that can generate errors in their corresponding mobile sdks (but not their web sdk ... weird) so we took care of this by explicitly serializing objects to JSON before they are inserted into the JWT.
I hope both of these examples help you track down the issue.
Perhaps I have been reading the wrong stuff, but one thing that all of the literatures that I have been reading seem to agree on is that: iOS does not allow background threads to run for longer than ten minutes. That seems to violate one of the greatest principles of app development: the internet should be invisible to your users. So here is a scenario.
A user is going through a tunnel or flying on an airplane, which causes no or unreliable network. At that instant, the user pulls out my email app, composes an email, and hits the send button.
Question: How do I the developer make sure that the email is sent when network becomes available? Of course I am using email as a general example, but in reality I am dealing with a very much simple http situation where my app needs to send a POST to my server.
Side Note: on android, I use Path’s priority job queue, which allows me to set it and forget it (i.e. as soon as there is network it sends my email).
another Side Note: I have been trying to use NSOperationQueue with AFNetworking, but does not do it.
What you want to achieve can be done using a background NSURLSession. While AFNetworking is based on NSURLSession I’m not quite sure if it can be used with a background session that runs while your app doesn’t. But you don’t really need this, NSURLSession is quite easy to use as is.
As a first step you need to create a session configuration for the background session:
let config = URLSessionConfiguration.background(withIdentifier: "de.5sw.test")
config.isDiscretionary = true
config.waitsForConnectivity = true
The isDiscretionary property allows the system to decide when to perform the data transfer. waitsForConnectivity (available since iOS 11) makes the system wait if there is no internet connection instead of failing immediately.
With that configuration object you can create your URL session. The important part is to specify a delegate as the closure-based callbacks get lost when the app is terminated.
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
To perform your upload you ask the session to create an upload task and then resume it. For the upload task you first create your URLRequest that specifies the URL and all needed headers. The actual data you want to upload needs to be written to a file. If you provide it as a Data or stream object it cannot be uploaded after your app terminates.
let task = session.uploadTask(with: request, fromFile: fileUrl)
task.resume()
To get notified of success or failure of your upload you need to implement the URLSessionDataDelegate method urlSession(_:task:didCompleteWithError:). If error is nil the transfer was successful.
The final piece that is missing is to handle the events that happened while your app was not running. To do this you implement the method application(_:handleEventsForBackgroundURLSession:completionHandler:) in your app delegate. When the system decides that you need to handles some events for background transfers it launches your app in the background and calls this method.
In there you need first store the completion handler and then recreate your URLSession with the same configuration you used before. This then calls it’s delegate for the events you need to handle as usual. Once it is done with the events it calls the delegate method urlSessionDidFinishEvents(forBackgroundURLSession:). From there you need to call the completion handler that was passed to your app delegate.
The session configuration provides some more options:
timeoutIntervalForResource: How long the system should try to perform your upload. Default is 7 days.
sessionSendsLaunchEvents: If false the app will not be launched to handle events. They will be handled when the user opens the app manually. Defaults is true.
Here is a small sample project that shows how everything fits together: https://github.com/5sw/BackgroundUploadDemo
Your app needs to store the data internally and then you either need something which will cause the app to run in the background (but you shouldn't necessarily add something specially if you don't already have a reason to be doing it) or to wait until the user next brings the app to the foreground - then you can check for a network connection and make the call.
Note that e-mail is very different to a POST, because you can pass an e-mail off to the system mail app to send for you but you can't do exactly the same thing with a POST.
Consider looking also at NSURLSessionUploadTask if you can use it.
In three words: you don't.
And that's actually a good thing. I certainly do not want to have to think and speculate about my last 20 apps, if they are still running in the background, using memory and battery and bandwidth. Furthermore, they would be killed if more memory is needed. How would the user be able to predict if it completed its task successfully? He can't, and need to open the app anyhow to check.
As for the email example, I'd go with showing the email as "pending" (i.e. not sent), until it transferred correctly. Make it obvious to the user that he has to come back later to fulfill the job.
While every developer thinks that his app has an extremely good reason for backgrounding, reality is, for the user in 99% it's just a pain. Can you say "task manager"? ;-)
I wrote a pod that does pretty much this - https://cocoapods.org/pods/OfflineRequestManager. You'd have to do some work listening to delegate callbacks if you want to monitor whether the request is in a pending or completed/failed state, but we've been using it to ensure that requests go out in poor or no connectivity scenarios.
The simplest use case would look something like the following, though most actual cases (saving to disk, specific request data, etc.) will have a few more hoops to jump through:
import OfflineRequestManager
class SimpleRequest: OfflineRequest {
func perform(completion: #escaping (Error?) -> Void) {
doMyNetworkRequest(withCompletion: { response, error in
handleResponse(response)
completion(error)
})
}
}
///////
OfflineRequestManager.defaultManager(queueRequest: SimpleRequest())