Fixing network connection loss in iOS with HttpClient
What’s causing the issue?
As a user I may receive phone calls or distracting notifications that "force" me to open another app, often a social media app. When this happens, iOS suspends the app causing active network requests to fail.
In this scenario, when developing MAUI iOS applications using HttpClient
, we encounter the The network connection was lost
exception as soon as the app comes back to the foreground.
var client = new HttpClient();
var responseTask = client.GetAsync("https://myservice.foo/my-endpoint");
// --> i.e. App goes to background because user receives a phone call
// ...
// --> After a while the user comes back to the app
using var response = await responseTask;
// <-- Exception: The network connection was lost
var content = await response.Content.ReadAsStringAsync();
This error not only creates a bad user experience, but it also poses a serious risk: your request might have already reached the server and altered your database.
If you don’t have proper checks in place on server side, a retry mechanism on your app may cause data duplication.
How can we solve the problem?
The Nalu core library (>= 8.2.1
) provides a solution to handle this use case without the need to change the logic of your application.
By installing Nalu.Core NuGet Package we can make use of the NSUrlBackgroundSessionHttpMessageHandler
within our HttpClient
instance.
#if IOS
HttpClient client = DeviceInfo.DeviceType == DeviceType.Virtual
? new() // iOS Simulator doesn't support background sessions
: new(NSUrlBackgroundSessionHttpMessageHandler());
#else
HttpClient client = new();
#endif
This handler uses NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration
under the hood:
It converts your
HttpRequestMessage
to aNSMutableUrlRequest
When needed, serializes your
Content
in a temporary fileCreates and starts a
NSUrlSessionTask
When the native request finishes, it creates an
HttpResponseMessage
for you and completes theresponseTask
(shown above)
The diagram below illustrates how this process generally works, although the order of Did*
events is not always guaranteed.
When the app goes to the background, iOS keeps processing the network requests. Once these requests are finished, it calls the application's handleEventsForBackgroundURLSession
event handler.
We have to add the following method to our AppDelegate
to react to such event and let the NSUrlBackgroundSessionHttpMessageHandler
take care of the now available responses.
[Export("application:handleEventsForBackgroundURLSession:completionHandler:")]
public virtual void HandleEventsForBackgroundUrl(
UIApplication application,
string sessionIdentifier,
Action completionHandler)
{
NSUrlBackgroundSessionHttpMessageHandler
.HandleEventsForBackgroundUrl(
application,
sessionIdentifier,
completionHandler);
}
What if my app crashes (or is terminated by the system) while a request is in progress?
I hope your app never crashes, but sometimes iOS might decide to close your app anyway under certain conditions. If you have a pending network request, the system will restart your app when the response is ready so you can handle it.
The problem is that since the app isn't running, there's no await
to wait for the response, and the system will discard it.
To handle this use case we need to do a couple of things:
We need a way to identify the request that was in progress
We need a way to react to the lost response
To identify the request, we have to add a special header to the request.
httpRequestMessage.Headers.Add(
NSUrlBackgroundSessionHttpMessageHandler.RequestIdentifierHeaderName,
"My request unique identifier 12345");
We can then register a singleton service implementing the INSUrlBackgroundSessionLostMessageHandler
interface to handle the lost response.
public class NSUrlBackgroundSessionLostMessageHandler
: INSUrlBackgroundSessionLostMessageHandler
{
public async Task HandleLostMessageAsync(
NSUrlBackgroundResponseHandle responseHandle)
{
try {
// "My request unique identifier 12345"
var requestIdentifier = responseHandle.RequestIdentifier;
using var response = await responseHandle.GetResponseAsync();
var content = await response.Content.ReadAsStringAsync();
// store content in a safe place related to the requestIdentifier
// `using` statement ends here acknowledging the response has been processed
} catch (Exception ex) {
// handle exception
}
}
}
builder.Services.AddSingleton<
INSUrlBackgroundSessionLostMessageHandler,
NSUrlBackgroundSessionLostMessageHandler
>();
What about other platforms?
Desktop applications (Windows and MacCatalyst) do not face this issue for obvious reasons. On the other hand, Android is very flexible and does not stop your HTTP requests when the app goes to the background.
Subscribe to my newsletter
Read articles from Alberto Aldegheri directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by