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 a NSMutableUrlRequest

  • When needed, serializes your Content in a temporary file

  • Creates and starts a NSUrlSessionTask

  • When the native request finishes, it creates an HttpResponseMessage for you and completes the responseTask (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:

  1. We need a way to identify the request that was in progress

  2. 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.

1
Subscribe to my newsletter

Read articles from Alberto Aldegheri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alberto Aldegheri
Alberto Aldegheri