Si desarrollas apps para iOS o macOS y necesitas hacer peticiones HTTP a través de un proxy — ya sea para web scraping, auditoría de precios, seguimiento de SERP o acceso a contenido regional — te habrás topado con que URLSession no expone una API sencilla para configurar proxies autenticados. A diferencia de libcurl o Requests en Python, Apple no ofrece un parámetro proxyURL directo. La solución pasa por URLSessionConfiguration.connectionProxyDictionary, cabeceras Proxy-Authorization y, en algunos casos, delegados de autenticación.
Esta guía explica cómo usar proxies en Swift con URLSession, cubriendo proxies HTTP y HTTPS, SOCKS5, geo-segmentación, concurrencia con async/await y patrones de producción como reintentos con backoff exponencial. Todos los ejemplos usan el gateway de ProxyHat en gate.proxyhat.com.
Por qué usar proxies en Swift con URLSession
URLSession es el cliente HTTP nativo de Apple para iOS, macOS, tvOS y watchOS. Internamente se apoya en URLSessionConfiguration, que expone una propiedad connectionProxyDictionary basada en las claves de CFNetwork. Apple no documenta exhaustivamente estas claves, pero corresponden a constantes como kCFNetworkProxiesHTTPEnable, kCFNetworkProxiesHTTPProxy y kCFStreamPropertySOCKSProxyHost.
El problema real no es configurar el proxy, sino autenticarse. Las claves kCFProxyUsernameKey y kCFProxyPasswordKey son poco fiables en URLSession: en muchas versiones de iOS se ignoran silenciosamente y la petición falla con un 407 Proxy Authentication Required. Por eso la estrategia recomendada es inyectar la cabecera Proxy-Authorization: Basic manualmente o implementar urlSession(_:didReceive:completionHandler:).
Los proxies residenciales son especialmente útiles cuando el endpoint de destino bloquea rangos de IPs de datacenter. Muchos servicios anti-bot — Cloudflare, Akamai, PerimeterX — filtran por ASN y rechazan tráfico de centros de datos conocidos. Una IP residencial, asociada a un ISP doméstico, tiene una probabilidad de bloqueo mucho menor. Para casos como seguimiento de SERP o web scraping esto marca la diferencia entre un 90% de éxito y un 30%.
Configurar un proxy HTTP/HTTPS en URLSession
El núcleo de la configuración es connectionProxyDictionary. Debes activar el proxy HTTP y HTTPS por separado, indicando host y puerto. Para ProxyHat, el gateway HTTP es gate.proxyhat.com en el puerto 8080.
import Foundation
func makeProxySession() -> URLSession {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
// Proxy HTTP
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort: 8080,
// Proxy HTTPS (túnel CONNECT)
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort: 8080
] as [String: Any]
return URLSession(configuration: config)
}
Para URLs https://, URLSession usa el método CONNECT para abrir un túnel a través del proxy. El proxy reenvía bytes cifrados sin inspeccionarlos, por lo que el certificado TLS del destino se valida de extremo a extremo. Esto significa que tu tráfico sigue siendo confidencial para el operador del proxy.
Autenticación con cabecera Proxy-Authorization
Dado que las claves kCFProxyUsernameKey/kCFProxyPasswordKey no son fiables, la forma más robusta de autenticarse es generar la cabecera Proxy-Authorization: Basic <base64> manualmente. El formato de usuario de ProxyHat permite incrustar país, ciudad y sesión:
import Foundation
struct ProxyAuth {
let username: String
let password: String
/// Ejemplo: user-country-US-city-newyork-session-abc123
static func geo(user: String,
country: String,
city: String? = nil,
session: String? = nil,
pass: String) -> ProxyAuth {
var u = "\(user)-country-\(country)"
if let city { u += "-city-\(city)" }
if let session { u += "-session-\(session)" }
return ProxyAuth(username: u, password: pass)
}
func basicHeader() -> String {
let raw = "\(username):\(password)"
let data = raw.data(using: .utf8)!.base64EncodedString()
return "Basic \(data)"
}
}
func makeRequest(url: URL, auth: ProxyAuth) -> URLRequest {
var req = URLRequest(url: url, timeoutInterval: 15)
req.setValue(auth.basicHeader(), forHTTPHeaderField: "Proxy-Authorization")
return req
}
Este enfoque funciona tanto para HTTP como para HTTPS, porque la cabecera Proxy-Authorization se envía antes del túnel CONNECT y nunca llega al servidor de origen. Ten cuidado de no confundirla con Authorization, que sí viaja al destino.
Alternativa: delegado de autenticación 407
Si prefieres no inyectar la cabecera manualmente, puedes implementar el método delegado que maneja el desafío 407. Es más verboso, pero respeta el flujo nativo de CFNetwork:
import Foundation
final class ProxyChallengeHandler: 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,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodHTTPProxy,
NSURLAuthenticationMethodHTTPSProxy:
let cred = URLCredential(user: username,
password: password,
persistence: .forSession)
completionHandler(.useCredential, cred)
default:
completionHandler(.performDefaultHandling, nil)
}
}
}
La ventaja de este delegado es que también cubre casos donde el proxy envía el 407 a mitad de una conexión persistente. La desventaja es que añade complejidad y un NSObject extra a mantener.
SOCKS5 en URLSession: puerto 1080
ProxyHat también expone un endpoint SOCKS5 en gate.proxyhat.com:1080. SOCKS5 opera en la capa de transporte y no interpreta HTTP, por lo que es útil cuando necesitas tunelizar protocolos arbitrarios o evitar ciertas inspecciones de proxies HTTP. En URLSession, las claves relevantes son kCFStreamPropertySOCKSProxyHost, kCFStreamPropertySOCKSProxyPort y las de credenciales.
import Foundation
func makeSOCKS5Session(user: String, pass: String) -> URLSession {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
kCFNetworkProxiesSOCKSEnable: true,
kCFStreamPropertySOCKSProxyHost: "gate.proxyhat.com",
kCFStreamPropertySOCKSProxyPort: 1080,
kCFStreamPropertySOCKSVersion: kCFStreamSocketSOCKSVersion5,
kCFStreamPropertySOCKSUser: user,
kCFStreamPropertySOCKSPassword: pass
] as [String: Any]
return URLSession(configuration: config)
}
A diferencia del proxy HTTP, en SOCKS5 las claves de usuario y contraseña sí suelen respetarse. Aun así, te recomendamos validar el comportamiento en el simulador y en dispositivo real, porque CFNetwork ha cambiado detalles entre versiones de iOS.
Ejemplo async/await con concurrencia y decodificación Codable
Para mostrar un caso realista, imaginemos una app que consulta un endpoint JSON regional (por ejemplo, precios de un e-commerce) desde múltiples países. Usaremos URLSession.shared.data(for:), un modelo Codable y un TaskGroup para lanzar peticiones concurrentes con sesiones distintas por país.
import Foundation
struct Price: Codable {
let sku: String
let price: Double
let currency: String
}
struct GeoConfig {
let country: String
let city: String?
let session: String
}
actor PriceFetcher {
let baseUser: String
let password: String
let endpoint = URL(string: "https://api.example.com/prices")!
init(baseUser: String, password: String) {
self.baseUser = baseUser
self.password = password
}
func fetchAll(configs: [GeoConfig]) async throws -> [Price] {
try await withThrowingTaskGroup(of: [Price].self) { group in
for cfg in configs {
group.addTask { try await self.fetch(cfg: cfg) }
}
var all: [Price] = []
for try await batch in group {
all.append(contentsOf: batch)
}
return all
}
}
private func fetch(cfg: GeoConfig) async throws -> [Price] {
let auth = ProxyAuth.geo(user: baseUser,
country: cfg.country,
city: cfg.city,
session: cfg.session,
pass: password)
let session = makeProxySession()
var req = URLRequest(url: endpoint, timeoutInterval: 20)
req.setValue(auth.basicHeader(), forHTTPHeaderField: "Proxy-Authorization")
let (data, response) = try await session.data(for: req)
guard let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Price].self, from: data)
}
}
Este patrón lanza tantas sesiones como países consultes. Cada URLSession mantiene su propio pool de conexiones, por lo que conviene reutilizar instancias si haces muchas peticiones al mismo destino. Para 100 sesiones concurrentes, considera limitar el httpMaximumConnectionsPerHost y usar un semámetro async para evitar saturar el proxy.
Errores comunes y casos límite
- Confundir Proxy-Authorization con Authorization. La primera se consume en el proxy; la segunda llega al servidor de origen. Si la mezclas, expondrás credenciales del proxy al destino.
- Olvidar activar HTTPS. Muchos desarrolladores setean solo
kCFNetworkProxiesHTTPEnabley se sorprenden de que las URLshttps://no pasen por el proxy. Debes activar tambiénkCFNetworkProxiesHTTPSEnable. - ATS bloquea certificados del proxy MITM. App Transport Security exige TLS 1.2+ y cifrados fuertes. Si tu proxy intenta inspeccionar HTTPS con un certificado propio, la conexión fallará salvo excepciones en
Info.plist. Los proxies residenciales de ProxyHat no inspeccionan tráfico, por lo que ATS no debería ser un problema. - Timeouts por latencia residencial. Una IP residencial puede añadir 50–300 ms respecto a un datacenter. Ajusta
timeoutIntervala 15–30 s para peticiones internacionales. - Reutilizar URLSessionDelegate sin retenerlo. Si el delegado se libera, la sesión pierde el manejador del 407 y las peticiones fallan.
Producción: reintentos, TLS y privacidad
Backoff exponencial con reintentos
Las redes residenciales son menos estables que los datacenters. Un patrón sólido es reintentar con backoff exponencial y jitter, respetando códigos 429 y 5xx:
import Foundation
func fetchWithRetry(session: URLSession,
request: URLRequest,
maxAttempts: Int = 4) async throws -> Data {
var attempt = 0
while true {
attempt += 1
do {
let (data, response) = try await session.data(for: request)
if let http = response as? HTTPURLResponse,
(500..<600).contains(http.statusCode) || http.statusCode == 429 {
throw URLError(.badServerResponse)
}
return data
} catch {
if attempt >= maxAttempts { throw error }
let base = pow(2.0, Double(attempt))
let jitter = Double.random(in: 0...0.5)
let delay = base + jitter
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
}
}
Delegado TLS personalizado
Si necesitas pinning de certificados o validación extra, implementa urlSession(_:didReceive:completionHandler:) para el desafío de servidor. Esto es independiente del proxy, pero conviene tenerlo en apps que scrapean endpoints con certificados auto-firmados o internos:
import Foundation
final class TLSPinner: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// Validar contra un conjunto de certificados anclados
completionHandler(.useCredential, URLCredential(trust: trust))
}
}
Privacidad en dispositivo
En iOS 14+, Apple exige el entitlement com.apple.developer.networking.networkextension para apps que actúan como proxies del sistema. Usar connectionProxyDictionary dentro de tu propia URLSession no requiere ese entitlement, porque la configuración es local a tu sesión y no afecta a otras apps. Aun así, si tu app enruta todo el tráfico del dispositivo, necesitarás el entitlement y revisar las App Store Review Guidelines.
Comparativa: proxy HTTP vs SOCKS5 en URLSession
| Aspecto | HTTP/HTTPS (8080) | SOCKS5 (1080) |
|---|---|---|
| Capa | Aplicación (HTTP CONNECT) | Transporte |
| Soporte URLSession | Estable, bien documentado | Funcional, menos documentado |
| Autenticación | Cabecera Proxy-Authorization o delegado 407 | Claves kCFStreamPropertySOCKSUser/Password |
| Geo-targeting | Sí, vía username | Sí, vía username |
| Latencia típica | +50–150 ms | +30–100 ms |
| Ideal para | APIs REST, scraping HTTP | Protocolos no HTTP, túneles arbitrarios |
Configuración de ProxyHat en Swift
ProxyHat ofrece proxies residenciales, móviles y datacenter accesibles por el mismo gateway. Para ver planes y precios, consulta el panel. La configuración en Swift se reduce a:
- Gateway:
gate.proxyhat.com - HTTP: puerto
8080 - SOCKS5: puerto
1080 - Usuario:
user-country-US-city-newyork-session-abc123 - Contraseña: tu clave de panel
El SDK de ProxyHat está disponible en Python y Node.js y usa el mismo gateway, por lo que puedes prototipar en backend y replicar la lógica en Swift. Para explorar ubicaciones disponibles, revisa el listado de países y ciudades soportados.
Consideraciones éticas y legales
Usar proxies para acceder a datos públicos es legítimo, pero debes respetar los términos de servicio de cada sitio, el archivo robots.txt y la legislación aplicable:
- EE. UU.: la Computer Fraud and Abuse Act (CFAA) penaliza el acceso no autorizado a sistemas protegidos. La jurisprudencia reciente (caso hiQ Labs vs LinkedIn, 2022) matiza que el scraping de datos públicos no constituye necesariamente acceso no autorizado, pero conviene asesorarse.
- UE: el RGPD protege datos personales. Si tu app recoge nombres, correos o IPs de usuarios, necesitas base legal y, en muchos casos, consentimiento.
- App Store: las guías de Apple prohíben apps que recopilen datos sin consentimiento o que interfieran con otros servicios. Prefiere APIs oficiales cuando existan.
- Frecuencia: limita la tasa de peticiones para no degradar el servicio objetivo. Un buen umbral es 1 petición por segundo por IP.
Regla práctica: si la web ofrece una API oficial con los datos que necesitas, úsala. Los proxies son para casos donde no hay API o esta no cubre tu región.
Puntos clave
connectionProxyDictionaryes la vía nativa para configurar proxies en URLSession; activa HTTP y HTTPS por separado.- Las claves de usuario/contraseña del proxy son poco fiables; usa
Proxy-Authorization: Basico un delegado 407. - SOCKS5 en puerto 1080 es útil para protocolos no HTTP y suele respetar las credenciales nativas.
- Los proxies residenciales reducen bloqueos en endpoints que filtran ASNs de datacenter.
- Implementa reintentos con backoff exponencial y jitter para tolerar la latencia residencial.
- Respeta robots.txt, ToS, CFAA, RGPD y las guías de la App Store.
Con estos bloques tienes una base sólida para integrar proxies en apps iOS y macOS. Empieza con un país, mide la tasa de éxito y escala con sesiones sticky cuando necesites consistencia de IP. Si tu backend ya usa Python o Node, el SDK de ProxyHat te permite compartir la misma configuración del gateway.






