Utiliser des proxies en Swift avec URLSession : guide développeur

Guide complet pour configurer des proxies résidentiels dans URLSession sur iOS et macOS : HTTP, SOCKS5, authentification, géo-ciblage, async/await, retry et bonnes pratiques de production.

Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

Vous développez une app iOS ou macOS qui doit accéder à des APIs ou des pages web derrière des restrictions géographiques, et vous découvrez que URLSession ne gère pas les proxies HTTP aussi simplement que vous l'espériez. Utiliser des proxies en Swift nécessite de plonger dans URLSessionConfiguration, les clés kCFNetworkProxies* et des stratégies d'authentification qui ne sont pas toujours documentées de façon évidente. Ce guide vous montre comment configurer correctement un proxy résidentiel — comme ProxyHat — avec URLSession, gérer l'authentification, le géo-ciblage, le SOCKS5, et déployer en production avec async/await.

Pourquoi utiliser des proxies en Swift avec URLSession

Sur iOS et macOS, URLSession est le client HTTP natif. Contrairement à Python où requests accepte un paramètre proxies={} explicite, Swift ne propose pas d'API directe pour injecter un proxy. À la place, il faut configurer un dictionnaire de proxy au niveau de la URLSessionConfiguration, via la propriété connectionProxyDictionary.

Les cas d'usage typiques où un proxy devient indispensable :

  • Scraping web en Swift sur des endpoints qui bloquent les plages d'IP de datacenter (AWS, GCP, OVH).
  • Accès à du contenu géo-restreint (catalogues e-commerce, résultats SERP localisés).
  • Tests QA depuis différentes régions sans changer de réseau physique.
  • Recherche sécurité nécessitant une rotation d'IP pour éviter le fingerprinting.

Les proxies résidentiels sont particulièrement adaptés parce qu'ils utilisent des adresses IP attribuées par des FAI réels, ce qui les rend indiscernables du trafic d'un utilisateur légitime. Les proxies datacenter sont plus rapides mais plus facilement détectés par les systèmes anti-bot. Consultez notre page des localisations pour voir la couverture géographique disponible.

Type de proxyVitesse typiqueDétection anti-botCoût relatifCas d'usage
Résidentiel200-800 msFaibleÉlevéScraping, SERP, contenu géo-restreint
Datacenter50-200 msÉlevéeFaibleAPIs publiques, monitoring simple
Mobile300-1000 msTrès faibleTrès élevéSocial media, apps mobiles

Configurer un proxy HTTP avec connectionProxyDictionary

La propriété connectionProxyDictionary de URLSessionConfiguration accepte un dictionnaire utilisant les clés kCFNetworkProxies* du framework CFNetwork d'Apple. Voici comment configurer un proxy HTTP pointant vers gate.proxyhat.com:8080 :

import Foundation

func makeProxySessionConfiguration(
    host: String = "gate.proxyhat.com",
    port: Int = 8080
) -> URLSessionConfiguration {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: true,
        kCFNetworkProxiesHTTPProxy: host,
        kCFNetworkProxiesHTTPPort: port,
        kCFNetworkProxiesHTTPSEnable: true,
        kCFNetworkProxiesHTTPSProxy: host,
        kCFNetworkProxiesHTTPSPort: port
    ] as [String: Any]
    return config
}

// Utilisation de base
let config = makeProxySessionConfiguration()
let session = URLSession(configuration: config)
let url = URL(string: "https://httpbin.org/ip")!
let task = session.dataTask(with: url) { data, response, error in
    if let error = error {
        print("Erreur proxy: \(error)")
        return
    }
    if let data = data, let body = String(data: data, encoding: .utf8) {
        print("Réponse: \(body)")
    }
}
task.resume()

Notez que vous devez activer à la fois les entrées HTTP et HTTPS, car URLSession tunnelle le trafic HTTPS à travers le proxy HTTP en utilisant la méthode CONNECT. Si vous omettez les clés kCFNetworkProxiesHTTPSEnable et associées, les requêtes HTTPS contourneront silencieusement le proxy.

Authentification et géo-ciblage avec Proxy-Authorization

C'est ici que les choses se compliquent. Les clés kCFProxyUsernameKey et kCFProxyPasswordKey existent dans CFNetwork, mais leur support dans URLSession est notoirement peu fiable sur iOS et macOS — elles peuvent être ignorées selon la version de l'OS. Deux approches fonctionnent en pratique :

Option 1 : En-tête Proxy-Authorization manuel

La méthode la plus robuste consiste à injecter directement l'en-tête Proxy-Authorization: Basic dans chaque requête. ProxyHat encode les options de géo-ciblage et de session directement dans le nom d'utilisateur, au format user-country-US-city-newyork-session-abc123.

import Foundation

struct ProxyHatConfig {
    let username: String
    let password: String
    let host: String = "gate.proxyhat.com"
    let port: Int = 8080

    // Construit l'en-tête Proxy-Authorization
    var proxyAuthHeader: String {
        let credentials = "\(username):\(password)"
        let base64 = Data(credentials.utf8).base64EncodedString()
        return "Basic \(base64)"
    }
}

func makeAuthenticatedRequest(
    proxyConfig: ProxyHatConfig,
    url: URL
) async throws -> Data {
    var config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: true,
        kCFNetworkProxiesHTTPProxy: proxyConfig.host,
        kCFNetworkProxiesHTTPPort: proxyConfig.port,
        kCFNetworkProxiesHTTPSEnable: true,
        kCFNetworkProxiesHTTPSProxy: proxyConfig.host,
        kCFNetworkProxiesHTTPSPort: proxyConfig.port
    ] as [String: Any]

    var request = URLRequest(url: url)
    request.setValue(proxyConfig.proxyAuthHeader, forHTTPHeaderField: "Proxy-Authorization")

    let session = URLSession(configuration: config)
    let (data, response) = try await session.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw URLError(.badServerResponse)
    }
    return data
}

// Exemple : proxy résidentiel US, New York, session persistante
let proxy = ProxyHatConfig(
    username: "user-country-US-city-newyork-session-abc123",
    password: "votre_mot_de_passe"
)

Task {
    do {
        let data = try await makeAuthenticatedRequest(
            proxyConfig: proxy,
            url: URL(string: "https://httpbin.org/ip")!
        )
        print(String(data: data, encoding: .utf8) ?? "")
    } catch {
        print("Échec: \(error)")
    }
}

Option 2 : Délégué URLSession pour le défi 407

Si le proxy renvoie un code 407 Proxy Authentication Required, vous pouvez intercepter ce défi via URLSessionDelegate. Cette approche est plus conforme au standard HTTP (RFC 7235 — Mozilla MDN) mais nécessite un délégué persistant :

import Foundation

final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
    let username: String
    let password: String

    init(username: String, password: String) {
        self.username = username
        self.password = password
    }

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        // Distinguer l'authentification proxy (407) du TLS
        if challenge.protectionSpace.authenticationMethod
            == NSURLAuthenticationMethodHTTPProxy {
            let credential = URLCredential(
                user: username,
                password: password,
                persistence: .forSession
            )
            return (.useCredential, credential)
        }
        // Laisser le système gérer le TLS par défaut
        return (.performDefaultHandling, nil)
    }
}

func makeProxySessionWithDelegate(proxy: ProxyHatConfig) -> URLSession {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: true,
        kCFNetworkProxiesHTTPProxy: proxy.host,
        kCFNetworkProxiesHTTPPort: proxy.port,
        kCFNetworkProxiesHTTPSEnable: true,
        kCFNetworkProxiesHTTPSProxy: proxy.host,
        kCFNetworkProxiesHTTPSPort: proxy.port
    ] as [String: Any]

    let delegate = ProxyAuthDelegate(
        username: "user-country-DE-city-berlin",
        password: proxy.password
    )
    return URLSession(configuration: config, delegate: delegate)
}

L'approche par délégué est plus propre mais peut échouer dans certains cas sur iOS 16+ où le défi proxy n'est pas toujours déclenché. En pratique, l'en-tête Proxy-Authorization manuel reste la solution la plus fiable.

Configurer un proxy SOCKS5 sur le port 1080

ProxyHat supporte également SOCKS5 sur le port 1080. Pour l'utiliser avec URLSession, remplacez les clés kCFNetworkProxiesHTTP* par les clés kCFStreamPropertySOCKSProxy* :

import Foundation

func makeSOCKS5Session(
    host: String = "gate.proxyhat.com",
    port: Int = 1080
) -> URLSession {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesSOCKSEnable: true,
        kCFNetworkProxiesSOCKSProxy: host,
        kCFNetworkProxiesSOCKSPort: port
    ] as [String: Any]
    return URLSession(configuration: config)
}

// Pour SOCKS5, l'authentification passe par le délégué
// car l'en-tête Proxy-Authorization n'est pas applicable
let session = makeSOCKS5Session()
let url = URL(string: "https://httpbin.org/ip")!
let task = session.dataTask(with: url) { data, _, error in
    if let data = data {
        print(String(data: data, encoding: .utf8) ?? "")
    }
}
task.resume()

SOCKS5 est utile quand le proxy HTTP échoue sur certains réseaux d'entreprise qui filtrent le trafic HTTP CONNECT. Gardez cependant que l'authentification SOCKS5 via URLSession est encore plus capricieuse que l'authentification HTTP proxy — le délégué avec NSURLAuthenticationMethodHTTPProxy (ou SOCKSProxy) est pratiquement obligatoire.

Exemple complet : async/await, Codable et TaskGroup

Voici un exemple de production qui combine le scraping web en Swift avec des proxies résidentiels, le décodage Codable, et la concurrence via TaskGroup. Cet exemple récupère des données depuis plusieurs endpoints en parallèle, avec rotation de sessions proxy pour chaque requête :

import Foundation

// Modèle de réponse
class IPResponse: Codable {
    let origin: String
}

struct ScrapedResult: Identifiable {
    let id = UUID()
    let endpoint: String
    let originIP: String
    let statusCode: Int
}

enum ScrapeError: Error {
    case invalidResponse
    case decodingFailed
    case requestFailed(String)
}

actor ProxyScraper {
    let baseConfig: ProxyHatConfig
    let session: URLSession

    init(proxyConfig: ProxyHatConfig) {
        self.baseConfig = proxyConfig

        let config = URLSessionConfiguration.default
        config.connectionProxyDictionary = [
            kCFNetworkProxiesHTTPEnable: true,
            kCFNetworkProxiesHTTPProxy: proxyConfig.host,
            kCFNetworkProxiesHTTPPort: proxyConfig.port,
            kCFNetworkProxiesHTTPSEnable: true,
            kCFNetworkProxiesHTTPSProxy: proxyConfig.host,
            kCFNetworkProxiesHTTPSPort: proxyConfig.port
        ] as [String: Any]
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        self.session = URLSession(configuration: config)
    }

    func scrapeEndpoint(_ urlString: String, sessionId: String) async throws -> ScrapedResult {
        guard let url = URL(string: urlString) else {
            throw ScrapeError.requestFailed("URL invalide: \(urlString)")
        }

        // Rotation de session via le nom d'utilisateur
        let rotatedUsername = "user-country-US-session-\(sessionId)"
        let proxyAuth = Data("\(rotatedUsername):\(baseConfig.password)".utf8)
            .base64EncodedString()

        var request = URLRequest(url: url)
        request.setValue("Basic \(proxyAuth)", forHTTPHeaderField: "Proxy-Authorization")
        request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
                         forHTTPHeaderField: "User-Agent")

        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw ScrapeError.invalidResponse
        }

        let ipResponse = try JSONDecoder().decode(IPResponse.self, from: data)
        return ScrapedResult(
            endpoint: urlString,
            originIP: ipResponse.origin,
            statusCode: httpResponse.statusCode
        )
    }

    func scrapeMultiple(endpoints: [String]) async -> [Result<ScrapedResult, Error>] {
        await withTaskGroup(of: Result<ScrapedResult, Error>.self) { group in
            for (index, endpoint) in endpoints.enumerated() {
                group.addTask {
                    do {
                        let result = try await self.scrapeEndpoint(
                            endpoint,
                            sessionId: "task-\(index)"
                        )
                        return .success(result)
                    } catch {
                        return .failure(error)
                    }
                }
            }
            var results: [Result<ScrapedResult, Error>] = []
            for await result in group {
                results.append(result)
            }
            return results
        }
    }
}

// Utilisation
let proxyConfig = ProxyHatConfig(
    username: "user-country-US",
    password: "votre_mot_de_passe"
)

let endpoints = [
    "https://httpbin.org/ip",
    "https://httpbin.org/headers",
    "https://httpbin.org/user-agent"
]

Task {
    let scraper = ProxyScraper(proxyConfig: proxyConfig)
    let results = await scraper.scrapeMultiple(endpoints: endpoints)
    for result in results {
        switch result {
        case .success(let scraped):
            print("✓ \(scraped.endpoint) → IP: \(scraped.originIP)")
        case .failure(let error):
            print("✗ Échec: \(error)")
        }
    }
}

Cet exemple illustre plusieurs concepts clés : la rotation de session via le nom d'utilisateur, la concurrence contrôlée avec TaskGroup, et le décodage type-safe avec Codable. Pour des cas d'usage plus avancés comme le suivi SERP ou le scraping web, adaptez les endpoints et le modèle Codable à votre cible.

Production : TLS, retry, ATS et confidentialité

Gestion du TLS avec URLSessionDelegate

Quand vous utilisez un proxy HTTPS, le tunnel CONNECT établit d'abord une connexion TCP vers le proxy, puis le TLS est négocié de bout en bout avec le serveur cible. Vous n'avez normalement pas besoin d'intervenir, mais si vous devez valider des certificats personnalisés ou gérer des pins, implémentez urlSession(_:didReceive:completionHandler:) pour le défi TLS :

import Foundation

final class TLSAwareDelegate: NSObject, URLSessionDelegate {
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            // Pour la production : valider la chaîne de certificats
            // Ne JAMAIS faire trustAll en production
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

Attention : accepter aveuglément tous les certificats en production est dangereux. Utilisez cette technique uniquement pour le débogage ou avec une logique de validation stricte.

Retry avec backoff exponentiel

Les proxies résidentiels peuvent échouer temporairement (timeout, IP temporairement bloquée). Un retry avec backoff exponentiel est essentiel :

import Foundation

func fetchWithRetry(
    url: URL,
    proxyConfig: ProxyHatConfig,
    maxRetries: Int = 3,
    baseDelay: UInt64 = 500_000_000 // 500ms en nanosecondes
) async throws -> Data {
    var lastError: Error?

    for attempt in 0..<maxRetries {
        do {
            let data = try await makeAuthenticatedRequest(
                proxyConfig: proxyConfig,
                url: url
            )
            return data
        } catch {
            lastError = error
            // Ne pas retry sur les erreurs non récupérables
            if let urlError = error as? URLError {
                switch urlError.code {
                case .badURL, .unsupportedURL:
                    throw error
                default:
                    break
                }
            }
            // Backoff exponentiel : 500ms, 1000ms, 2000ms
            let delay = baseDelay * UInt64(1 << attempt)
            try? await Task.sleep(nanoseconds: delay)
            print("Retry attempt \(attempt + 1) après \(delay / 1_000_000)ms")
        }
    }
    throw lastError ?? URLError(.unknown)
}

App Transport Security (ATS)

Apple impose App Transport Security (ATS) qui requiert TLS 1.2+ pour toutes les connexions. Les proxies HTTP ne violent pas ATS car le trafic vers le serveur cible reste chiffré en TLS de bout en bout via CONNECT. Cependant, si votre proxy utilise un certificat auto-signé pour l'inspection TLS (man-in-the-middle), vous devrez configurer NSExceptionDomains dans votre Info.plist — ce qui peut compromettre la sécurité. ProxyHat ne fait pas d'inspection TLS, donc ATS reste intact.

Confidentialité sur l'appareil

Sur iOS, l'adresse IP de l'utilisateur est considérée comme une donnée personnelle selon les guidelines de l'App Store. Si votre app envoie le trafic utilisateur à travers un proxy tiers, vous devez :

  • En informer l'utilisateur dans votre politique de confidentialité.
  • Obtenir le consentement si l'app collecte des données via le proxy.
  • Ne pas intercepter le trafic d'autres apps (ce qui nécessiterait un VPN entitlement restrictif).

Bonnes pratiques et éthique

L'utilisation de proxies pour accéder à des données publiques est légale dans la plupart des juridictions, mais elle doit respecter les conditions d'utilisation des plateformes. Aux États-Unis, le Computer Fraud and Abuse Act (CFAA) peut s'appliquer si vous contournez des mesures d'authentification. Dans l'Union Européenne, le RGPD (GDPR) protège les données personnelles, y compris les adresses IP.

Règles de base :

  • Préférez les APIs officielles quand elles existent. Un proxy ne doit pas servir à contourner des limites d'API raisonnables.
  • Respectez robots.txt et les conditions d'utilisation.
  • Limitez le débit de vos requêtes — 1 à 2 requêtes par seconde par IP est une bonne pratique pour le scraping respectueux.
  • N'extrayez pas de données personnelles sans base légale.
  • Si vous publiez sur l'App Store, assurez-vous que votre usage des proxies est transparent et conforme aux App Store Review Guidelines.

Le SDK ProxyHat existe en Python et Node.js et reflète exactement le même gateway gate.proxyhat.com. Les patterns d'authentification et de géo-ciblage sont identiques — seules les bibliothèques HTTP changent. Pour le tarif, consultez notre page de prix.

Points clés à retenir

  • connectionProxyDictionary avec les clés kCFNetworkProxiesHTTP* est la seule façon native de configurer un proxy HTTP dans URLSession.
  • L'en-tête Proxy-Authorization: Basic manuel est plus fiable que kCFProxyUsernameKey/PasswordKey sur iOS/macOS.
  • Le géo-ciblage et les sessions se codent dans le nom d'utilisateur : user-country-US-city-newyork-session-abc123.
  • SOCKS5 utilise les clés kCFNetworkProxiesSOCKS* sur le port 1080, avec authentification par délégué.
  • En production : retry avec backoff exponentiel, timeout configurable, et respect d'ATS.
  • L'éthique et la légalité priment — préférez les APIs officielles, respectez robots.txt, le RGPD et les guidelines App Store.

FAQ

Qu'est-ce que l'utilisation de proxies en Swift ?

L'utilisation de proxies en Swift consiste à router le trafic HTTP/HTTPS de URLSession via un serveur proxy intermédiaire en configurant URLSessionConfiguration.connectionProxyDictionary avec les clés kCFNetworkProxies*. Cela permet de masquer l'IP d'origine, de géo-localiser les requêtes, et d'accéder à des endpoints qui bloquent les IP de datacenter.

Pourquoi utiliser des proxies avec URLSession sur iOS ?

Les proxies sont nécessaires quand les endpoints cibles bloquent les plages d'IP de datacenter (fréquent pour le scraping web, le suivi SERP, ou l'accès à du contenu géo-restreint). Les proxies résidentiels offrent des adresses IP de FAI réels, indiscernables du trafic légitime, avec un taux de succès typique de 90%+ contre 30-50% pour les proxies datacenter sur les sites protégés.

Quel type de proxy fonctionne le mieux avec Swift et URLSession ?

Les proxies résidentiels HTTP sur le port 8080 sont le meilleur choix pour URLSession car ils offrent le meilleur équilibre entre fiabilité d'authentification (via l'en-tête Proxy-Authorization), compatibilité ATS, et discrétion anti-bot. SOCKS5 sur le port 1080 est une alternative utile dans les environnements réseau restrictifs, mais l'authentification est plus complexe à gérer.

Comment éviter les blocages quand on utilise des proxies en Swift ?

Pour éviter les blocages : (1) utilisez des proxies résidentiels plutôt que datacenter, (2) faites tourner les sessions via le nom d'utilisateur (session-abc123), (3) ajoutez un délai entre les requêtes (1-2 req/s par IP), (4) implémentez un retry avec backoff exponentiel, (5) définissez un User-Agent réaliste, et (6) respectez robots.txt et les conditions d'utilisation.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog