Tester l’envoi de fichiers iOS en arrière-plan avec URLSession

Tester l’envoi de fichiers iOS en arrière-plan avec URLSession

Sur une machine de développement distante, une erreur d’interprétation est fréquente : un fichier s’envoie correctement tant que l’application iOS est au premier plan dans le simulateur, et l’on en déduit que l’envoi survivra au passage en arrière-plan. Or un envoi en arrière-plan fait intervenir un fichier sur disque, une tâche de transfert prise en charge par le système et des rappels à traiter lorsque l’application reprend. Vérifier que la requête est partie ne suffit pas. Nous allons d’abord valider le parcours avec un récepteur local sur le Mac distant, puis préciser les contrôles à réserver à un appareil réel.

Définir le périmètre de validation

Ce test porte sur une tâche d’envoi de fichier créée avec URLSessionConfiguration.background, et non sur des données Data conservées en mémoire en attendant le retour du réseau. Préparez un fichier d’essai de taille facilement identifiable, sans véritable identifiant ni secret, puis notez son nombre d’octets et son empreinte SHA-256. Le serveur doit au minimum consigner la méthode de la requête, le nombre d’octets reçus et le statut renvoyé. Côté client, consignez séparément l’identifiant de la tâche, l’erreur de transfert et le code de statut HTTP.

Si l’erreur de didCompleteWithError vaut nil, cela signifie seulement qu’aucune erreur n’a été signalée au niveau du transfert. Le serveur peut tout de même répondre par un code 4xx ou 5xx. La validation doit donc aussi examiner la réponse HTTP.

Le simulateur permet de vérifier ce parcours de base. Il ne peut pas prouver à la place d’un appareil réel que le système relancera l’application dans certaines conditions, ni reproduire les changements de réseau mobile réels.

Démarrer un récepteur minimal sur le Mac

Enregistrez le script suivant sous receiver.py, puis exécutez-le sur le Mac qui fait tourner le simulateur. Il n’écoute que sur l’adresse de bouclage, limite la taille de chaque requête et écrit les données reçues dans un chemin de test fixe. Ne l’utilisez pas comme service d’envoi ouvert sur Internet.

from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

LIMIT = 10 * 1024 * 1024
OUTPUT = Path("/tmp/ios-upload-received.bin")

class Receiver(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        if self.path != "/upload" or not 0 < length <= LIMIT:
            self.send_error(400)
            return
        body = self.rfile.read(length)
        if len(body) != length:
            self.send_error(400)
            return
        OUTPUT.write_bytes(body)
        self.send_response(201)
        self.end_headers()

HTTPServer(("127.0.0.1", 8765), Receiver).serve_forever()

Lancez-le avec python3 receiver.py. Pour que l’application de test accède à http://127.0.0.1:8765/upload, configurez uniquement dans la version de test une exception App Transport Security de portée limitée pour localhost ou l’hôte de bouclage utilisé ; la version distribuée doit continuer à employer HTTPS. Si le simulateur ne parvient pas à se connecter, vérifiez d’abord que le récepteur tourne toujours et que le port correspond, puis examinez l’exception configurée. Ne modifiez pas d’emblée la logique d’envoi.

Créer une tâche en arrière-plan à partir d’un fichier sur disque

L’exemple suivant montre les éléments essentiels à la création de la tâche. Le code appelant doit fournir l’URL d’un fichier dont l’écriture est terminée et que le processus peut lire. endpoint doit provenir de la configuration de test : ne figez pas l’adresse locale dans le code distribué. L’identifiant de la session en arrière-plan doit rester stable et être propre à ce parcours d’envoi, plutôt que d’être régénéré à chaque lancement.

import Foundation

final class UploadCoordinator: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(
            withIdentifier: "com.example.upload-check"
        )
        config.isDiscretionary = false
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    func upload(file: URL, endpoint: URL) {
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        session.uploadTask(with: request, fromFile: file).resume()
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        let status = (task.response as? HTTPURLResponse)?.statusCode
        let accepted = error == nil && status == 201
        recordResult(taskID: task.taskIdentifier, accepted: accepted)
    }

    func urlSessionDidFinishEvents(
        forBackgroundURLSession session: URLSession
    ) {
        finishPendingBackgroundEvents()
    }

    private func recordResult(taskID: Int, accepted: Bool) {
        // Transmettre le résultat au mécanisme de persistance de l’application
    }

    private func finishPendingBackgroundEvents() {
        // Appeler le gestionnaire de fin fourni par le système après la mise à jour de l’état
    }
}

Les deux méthodes privées de l’exemple sont des points d’intégration à l’application, pas une implémentation complète de la fin de l’envoi. Dans l’application, le résultat persisté doit conserver le lien entre la tâche et le fichier métier. Lorsque le délégué de l’application reçoit des événements de la session en arrière-plan, recréez la session avec le même identifiant, rattachez son délégué et conservez sous cet identifiant le gestionnaire de fin fourni par le système. Appelez ce gestionnaire une fois les événements traités et l’état de l’interface mis à jour. Ne supprimez pas un fichier métier encore non vérifié sur la seule foi d’un rappel de réussite.

Valider par étapes, sans confondre arrêt forcé et arrière-plan

Commencez avec l’application au premier plan et envoyez le fichier d’essai. Après la réponse 201 du récepteur, comparez l’empreinte SHA-256 de /tmp/ios-upload-received.bin à celle du fichier d’origine. Leur concordance confirme que le contenu attendu a été reçu intégralement lors de cet essai. Prenez ensuite un autre fichier d’essai, lancez son envoi, puis revenez à l’écran d’accueil depuis l’interface du simulateur. Attendez le rappel et vérifiez à nouveau le fichier. N’utilisez pas de commande qui force l’arrêt du processus pour simuler un simple passage en arrière-plan : une fermeture forcée par l’utilisateur n’est pas équivalente à une reprise après la prise en charge de la tâche par le système.

Point de contrôle Éléments à consigner
Création de la tâche Identifiant de session, identifiant de tâche, chemin et nombre d’octets du fichier source
Réception côté serveur Statut HTTP, nombre réel d’octets reçus, empreinte SHA-256 du fichier
Fin côté client Erreur de transfert et statut HTTP consignés séparément
Reprise de l’application Recréation de la session, arrivée des rappels et ordre d’appel du gestionnaire de fin

La validation sur appareil réel doit couvrir séparément le verrouillage de l’écran, la reprise après une interruption du réseau, le changement de type de réseau et la relance de l’application après son arrêt par le système. Testez à part la fermeture forcée par l’utilisateur, sans assimiler son résultat au comportement normal en arrière-plan. Si vous ne pouvez pas connecter d’appareil réel au Mac distant, transmettez les preuves obtenues dans le simulateur et la liste des tests restants à un collègue qui dispose d’un appareil. N’écrivez pas dans le compte rendu que les tests sur appareil réel sont validés.

Diagnostiquer les échecs et terminer le test

Si le serveur ne reçoit aucune requête, vérifiez d’abord que l’URL du fichier désigne un fichier ordinaire existant, puis contrôlez l’adresse de test, le port et la configuration de sécurité des transports. Si le fichier arrive mais que le client indique un échec, examinez le statut de la réponse : l’exemple n’accepte que 201. L’absence d’erreur de transfert ne signifie pas que le serveur a accepté le fichier. Si aucun rappel n’arrive après la reprise de l’application, vérifiez que la session en arrière-plan a été recréée avec son identifiant initial, que l’objet délégué est toujours conservé et que le gestionnaire de fin n’a pas été appelé trop tôt.

Enfin, arrêtez le récepteur de test, supprimez /tmp/ios-upload-received.bin et nettoyez les fichiers sources ainsi que les exceptions réseau réservés aux tests. Le parcours de production doit aussi fixer une limite aux nouvelles tentatives après échec, définir le traitement des envois répétés côté serveur et ne supprimer les fichiers qu’après confirmation de la persistance du résultat. Vous transmettrez ainsi à l’ingénieur suivant non pas un simple « l’envoi en arrière-plan fonctionne », mais des étapes reproductibles, des preuves vérifiables et la liste des contrôles restant à effectuer sur appareil réel.

Questions fréquentes

Une URLSession en arrière-plan peut-elle envoyer directement un objet Data en mémoire ?

Pour cette procédure, utilisez un fichier : écrivez d’abord les données sur disque, puis créez la tâche avec uploadTask(with:fromFile:).

Un succès dans le simulateur prouve-t-il que la relance fonctionne sur appareil ?

Non. Le simulateur permet de vérifier requête, réponse et rappels de base ; testez séparément sur appareil la relance par le système, les changements de réseau et l’arrêt forcé par l’utilisateur.

Machine physique dédiée

Préparez un Mac pour votre prochain projet de développement à distance

Choisissez un modèle et un emplacement selon vos besoins, puis connectez-vous via le partage d’écran, VNC ou SSH. Les prix et disponibilités réels sont ceux indiqués en temps réel dans la console.

Configurer un Mac distant