
Urlsession Code Review
- 98 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with ai & agent building tasks.
About
urlsession-code-review is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- urlsession-code-review
- AI & Agent Building
- AI-coding skill
Urlsession Code Review by the numbers
- 98 all-time installs (skills.sh)
- Ranked #4,469 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill urlsession-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 98 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with ai & agent building tasks.
Files
URLSession Code Review
Quick Reference
| Topic | Reference |
|---|---|
| Async/Await | async-networking.md |
| Requests | request-building.md |
| Errors | error-handling.md |
| Caching | caching.md |
Review Checklist
Response Validation
- [ ] HTTP status codes validated - URLSession does NOT throw on 404/500
- [ ] Response cast to HTTPURLResponse before checking status
- [ ] Both transport errors (URLError) and HTTP errors handled
Memory & Resources
- [ ] Downloaded files moved/deleted (async API doesn't auto-delete)
- [ ] Sessions with delegates call
finishTasksAndInvalidate() - [ ] Long-running tasks use
[weak self] - [ ] Stored Task references cancelled when appropriate
Configuration
- [ ]
timeoutIntervalForResourceset (default is 7 days!) - [ ] URLCache sized adequately (default 512KB too small)
- [ ] Sessions reused for connection pooling
Background Sessions
- [ ] Unique identifier (especially with app extensions)
- [ ] File-based uploads (not data-based)
- [ ] Delegate methods used (not completion handlers)
Security
- [ ] No hardcoded secrets (use Keychain)
- [ ] Header values sanitized for CRLF injection
- [ ] Query params via URLComponents (not string concat)
Hard gates (before reporting findings)
Complete in order. Do not advance while a prior gate is open.
1. Scope — Pass: You name at least one file under review where URLSession, URLRequest, HTTPURLResponse / URLResponse, URLCache, or URLError appears on a networking path. If none apply, stop with “out of scope.” 2. HTTP vs transport — Pass: Before claiming missing HTTP status handling or “404 treated as success,” you cite file:line for the completion/async/for await path that receives response and state whether HTTPURLResponse is cast and statusCode is checked (or cite the helper that does). If you cannot see the handler, say unknown and ask for it—do not assume. 3. Session lifecycle — Pass: For a custom URLSession with a delegate, you cite finishTasksAndInvalidate() or the documented long-lived/singleton pattern you rely on; for .shared, say so if the finding depends on configuration. Skip if only ad hoc URLSession.shared one-shots with no delegate issues. 4. Background or file transfer (if applicable) — Pass: If URLSessionConfiguration.background, downloadTask, or app-extension–scoped sessions appear, findings cite identifier uniqueness, delegate vs completion-handler usage, or file URLs as required. If none of those APIs appear, mark N/A and continue. 5. Severity and checklist — Pass: Every Critical item includes file:line and names which Review Checklist subsection it violates (e.g. Response Validation, Background Sessions). Lower-severity items still name the file(s) they are drawn from.
Output Format
### Critical
1. [FILE:LINE] Missing HTTP status validation
- Issue: 404/500 responses not treated as errors
- Fix: Check `httpResponse.statusCode` is 200-299URLSession Async/Await Reference
Minimum deployment: iOS 15+, macOS 12+
Quick Reference
Core Async Methods
| Method | Returns | Use Case |
|---|---|---|
data(from: URL) | (Data, URLResponse) | Simple GET requests |
data(for: URLRequest) | (Data, URLResponse) | Configured requests (POST, headers) |
download(from: URL) | (URL, URLResponse) | Large files to disk |
download(for: URLRequest) | (URL, URLResponse) | Large files with custom request |
upload(for: URLRequest, from: Data) | (Data, URLResponse) | Upload data in memory |
upload(for: URLRequest, fromFile: URL) | (Data, URLResponse) | Upload file from disk |
bytes(from: URL) | (AsyncBytes, URLResponse) | Streaming response body |
Data Tasks
// Basic GET
func fetchData(from url: URL) async throws -> Data {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
return data
}
// POST with URLRequest
func postData<T: Encodable>(_ body: T, to url: URL) async throws -> Data {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
// Validate response...
return data
}Download Tasks
func downloadFile(from url: URL, to destination: URL) async throws {
let (tempURL, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
// CRITICAL: Move or delete the file - it is NOT auto-deleted
try FileManager.default.moveItem(at: tempURL, to: destination)
}Key difference: The async/await download API does NOT automatically delete temporary files.
Streaming with AsyncBytes
// Line-by-line processing
func streamLines(from url: URL) async throws {
let (bytes, _) = try await URLSession.shared.bytes(from: url)
for try await line in bytes.lines {
processLine(line)
}
}
// Server-Sent Events
func subscribeToEvents(url: URL) async throws {
let (bytes, _) = try await URLSession.shared.bytes(from: url)
for try await line in bytes.lines {
if line.hasPrefix("data: ") {
let jsonString = String(line.dropFirst(6))
// Parse and handle event
}
}
}Task Cancellation
Task cancellation automatically propagates to URLSession requests.
class DataLoader {
private var loadTask: Task<Data, Error>?
func load(from url: URL) {
loadTask?.cancel() // Cancel previous request
loadTask = Task {
try await URLSession.shared.data(from: url).0
}
}
}SwiftUI's .task modifier automatically cancels when the view disappears.
Memory Management
Tasks implicitly capture self strongly. Use [weak self] for long-running tasks:
downloadTask = Task { [weak self] in
guard let url = self?.downloadURL else { return }
let (data, _) = try await URLSession.shared.data(from: url)
self?.processData(data)
}Critical Anti-Patterns
1. Not Checking HTTP Status Codes
// BAD: 404 does not throw an error
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(Model.self, from: data) // Crashes on error HTML
// GOOD: Validate response
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.serverError
}2. Forgetting to Delete Downloaded Files
// BAD: Temporary file wastes storage
let (tempURL, _) = try await URLSession.shared.download(from: url)
// File is never moved or deleted
// GOOD: Always handle temporary file
let (tempURL, _) = try await URLSession.shared.download(from: url)
defer { try? FileManager.default.removeItem(at: tempURL) }
let data = try Data(contentsOf: tempURL)3. Upload Without HTTP Method
// BAD: GET cannot have a body
var request = URLRequest(url: url)
try await URLSession.shared.upload(for: request, from: data) // Fails
// GOOD: Set HTTP method
request.httpMethod = "POST"
try await URLSession.shared.upload(for: request, from: data)4. Storing Tasks Without Cancellation
// BAD: Tasks accumulate
func search(query: String) {
Task { let results = try await performSearch(query) }
}
// GOOD: Cancel previous task
private var searchTask: Task<Void, Never>?
func search(query: String) {
searchTask?.cancel()
searchTask = Task {
guard !Task.isCancelled else { return }
let results = try? await performSearch(query)
}
}5. Strong Self in Infinite Loops
// BAD: Permanent memory leak
listenerTask = Task {
for try await line in bytes.lines {
self.handleEvent(line) // Never deallocates
}
}
// GOOD: Weak self
listenerTask = Task { [weak self] in
for try await line in bytes.lines {
guard let self else { return }
self.handleEvent(line)
}
}Review Questions
- [ ] Are HTTP status codes validated (not just assuming success)?
- [ ] Are downloaded files moved/deleted after use?
- [ ] Are upload requests setting HTTP method (POST/PUT)?
- [ ] Are long-running tasks using
[weak self]? - [ ] Are stored Task references cancelled when appropriate?
- [ ] Is cancellation handled in
viewWillDisappear? - [ ] Is SwiftUI's
.taskmodifier used instead of manual Task management? - [ ] For streaming, is response status checked before iterating?
URLSession Caching and Configuration Reference
Quick Reference
URLSessionConfiguration Types
| Type | Persistence | Use Case |
|---|---|---|
.default | Disk cache, cookies | Normal networking |
.ephemeral | Memory only | Privacy-sensitive |
.background(withIdentifier:) | System-managed | Large transfers |
Cache Policies
| Policy | Behavior | When to Use |
|---|---|---|
.useProtocolCachePolicy | Follows HTTP headers | Default |
.reloadIgnoringLocalCacheData | Always fetch fresh | Fresh data required |
.returnCacheDataElseLoad | Cache first | Offline-first |
.returnCacheDataDontLoad | Cache only | Strict offline |
Timeout Defaults
| Property | Default | Typical Setting |
|---|---|---|
timeoutIntervalForRequest | 60s | 30-60s |
timeoutIntervalForResource | 7 days | 2-5 minutes |
URLCache Sizing
| Type | Default | Recommended |
|---|---|---|
| Memory | 512 KB | 20 MB |
| Disk | 10 MB | 100 MB |
URLCache Configuration
// Default cache is too small - configure early in app lifecycle
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
URLCache.shared = URLCache(
memoryCapacity: 20 * 1024 * 1024, // 20 MB
diskCapacity: 100 * 1024 * 1024, // 100 MB
directory: nil
)
return true
}Cache rules: Response must be <= 5% of disk cache size to be cached. (Apple Developer Documentation))
Session Configuration
Default Configuration
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 300.0 // Not 7 days!
config.waitsForConnectivity = true
let session = URLSession(configuration: config)Ephemeral (Privacy Mode)
// No disk persistence - RAM only
let session = URLSession(configuration: .ephemeral)Background Configuration
let config = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.backgroundSession"
)
config.isDiscretionary = false // Start immediately
config.sessionSendsLaunchEvents = true
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)Background Session Implementation
AppDelegate Handler (Required)
var backgroundCompletionHandler: (() -> Void)?
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
backgroundCompletionHandler = completionHandler
}Delegate Methods
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
// Move file immediately - location deleted after method returns
try? FileManager.default.moveItem(at: location, to: permanentURL)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
self.backgroundCompletionHandler?()
self.backgroundCompletionHandler = nil
}
}Connection Pooling
// CORRECT: Reuse sessions for HTTP/2 multiplexing
class NetworkService {
static let shared = NetworkService()
private let session = URLSession(configuration: .default)
}
// ANTI-PATTERN: New session per request - loses pooling
func badFetch(_ url: URL) async throws -> Data {
let session = URLSession(configuration: .default) // Bad!
return try await session.data(from: url).0
}Critical Anti-Patterns
1. Memory Leak from Strong Delegate
// BUG: URLSession retains delegate forever
class LeakyManager {
var session: URLSession!
init() {
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
// deinit never called - memory leak
}
// CORRECT: Invalidate session
class CorrectManager {
var session: URLSession!
init() {
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
deinit {
session.finishTasksAndInvalidate()
}
}2. Background Session Identifier Conflicts
// BUG: Same identifier in app and extension
// Main app
let config = URLSessionConfiguration.background(withIdentifier: "downloads")
// Extension (CONFLICT!)
let config = URLSessionConfiguration.background(withIdentifier: "downloads")
// CORRECT: Unique per process
"com.yourapp.main.downloads"
"com.yourapp.extension.downloads"3. Data-Based Background Uploads
// BUG: Data uploads don't persist in background
backgroundSession.uploadTask(with: request, from: data) // Fails!
// CORRECT: File-based uploads
let fileURL = saveDataToFile(data)
backgroundSession.uploadTask(with: request, fromFile: fileURL)4. Completion Handlers in Background Sessions
// BUG: Completion handlers not called
backgroundSession.dataTask(with: url) { data, _, _ in
// Never executed!
}
// CORRECT: Use delegate methods only5. Inadequate Cache Size
// BUG: Default 512KB memory, 10MB disk - too small
let session = URLSession.shared
// CORRECT: Configure adequate cache
URLCache.shared = URLCache(
memoryCapacity: 20 * 1024 * 1024,
diskCapacity: 100 * 1024 * 1024,
directory: nil
)6. Battery Drain from Immediate Transfers
// ANTI-PATTERN: Non-urgent but immediate
config.isDiscretionary = false // Immediate regardless of conditions
// CORRECT: Let system optimize
config.isDiscretionary = true // Waits for WiFi, charging7. Missing Background Event Handler
// BUG: No handleEventsForBackgroundURLSession
class IncompleteAppDelegate: UIResponder, UIApplicationDelegate {
// App never notified of completion
}8. Unresumed Tasks
// BUG: Task created but never resumed
task = session.dataTask(with: url) { ... }
// MISSING: task.resume()
// Completion handler retained indefinitely
// CORRECT
task.resume() // Always callReview Questions
Cache
- [ ] Is URLCache configured with adequate capacity?
- [ ] Is cache configured before network calls?
- [ ] Is ephemeral config used for sensitive data?
Session Management
- [ ] Are sessions reused (not created per request)?
- [ ] Is session invalidated when done?
- [ ] Are timeouts configured (not 7-day default)?
Background Sessions
- [ ] Is identifier unique (especially with extensions)?
- [ ] Is
handleEventsForBackgroundURLSessionimplemented? - [ ] Is
urlSessionDidFinishEventscalling completion handler? - [ ] Are uploads file-based (not data-based)?
- [ ] Are delegate methods used (not completion handlers)?
- [ ] Is
isDiscretionaryset for non-urgent transfers? - [ ] Is background session at app level (not ViewController)?
Memory
- [ ] Is session delegate invalidated to break retain cycle?
- [ ] Are tasks always resumed after creation?
URLSession Error Handling Reference
Quick Reference
URLError Codes
| Code | Name | Retryable | User Message |
|---|---|---|---|
| -1009 | notConnectedToInternet | No* | "You're offline" |
| -1001 | timedOut | Yes | "Request timed out" |
| -999 | cancelled | No | (Silent) |
| -1003 | cannotFindHost | Yes | "Unable to reach server" |
| -1004 | cannotConnectToHost | Yes | "Unable to connect" |
| -1005 | networkConnectionLost | Yes | "Connection lost" |
| -1200 | secureConnectionFailed | No | "Security error" |
*Wait for network to reconnect
HTTP Status Codes
| Range | Category | Retryable | Handling |
|---|---|---|---|
| 200-299 | Success | N/A | Process response |
| 400 | Bad Request | No | Show validation error |
| 401 | Unauthorized | No | Re-authenticate |
| 404 | Not Found | No | Show not found |
| 429 | Too Many Requests | Yes | Respect Retry-After |
| 500-599 | Server Error | Yes | Retry with backoff |
Transport vs HTTP Errors
Critical: URLSession does NOT treat non-2xx status codes as errors automatically.
// Transport errors via error parameter
if let error = error as? URLError {
switch error.code {
case .notConnectedToInternet: // Device offline
case .timedOut: // Request timed out
case .cancelled: // User cancelled
default: break
}
}
// HTTP errors via status code (MUST check manually)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
// Server returned error - data may contain error body
}Response Validation
func validateResponse(_ data: Data?, _ response: URLResponse?, _ error: Error?) throws -> Data {
// 1. Transport errors first
if let error = error {
throw NetworkError.transport(error)
}
// 2. Validate response type
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
// 3. Check status code
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(httpResponse.statusCode, data)
}
// 4. Validate data
guard let data = data, !data.isEmpty else {
throw NetworkError.noData
}
return data
}Retry Strategy
Determining Retryability
extension URLError.Code {
var isRetryable: Bool {
switch self {
case .timedOut, .cannotFindHost, .cannotConnectToHost,
.networkConnectionLost, .dnsLookupFailed:
return true
case .notConnectedToInternet, .cancelled,
.secureConnectionFailed, .userAuthenticationRequired:
return false
default:
return false
}
}
}
extension Int {
var isRetryableStatusCode: Bool {
[408, 429, 500, 502, 503, 504].contains(self)
}
}Exponential Backoff with Jitter
struct RetryConfiguration {
let maxRetries: Int = 3
let baseDelay: TimeInterval = 1.0
let maxDelay: TimeInterval = 30.0
func delay(for attempt: Int) -> TimeInterval {
let exponential = baseDelay * pow(2.0, Double(attempt))
let clamped = min(exponential, maxDelay)
let jitter = Double.random(in: 0...(0.1 * clamped))
return clamped + jitter
}
}Retry-After Header
func retryDelay(from response: HTTPURLResponse, fallback: TimeInterval) -> TimeInterval {
if let retryAfter = response.value(forHTTPHeaderField: "Retry-After"),
let seconds = Double(retryAfter) {
return seconds
}
return fallback
}Network Conditions
waitsForConnectivity (Recommended)
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true // Wait instead of failing
config.timeoutIntervalForResource = 300 // Don't use 7-day default
// Delegate for UI feedback
func urlSession(_ session: URLSession,
taskIsWaitingForConnectivity task: URLSessionTask) {
// Show "waiting for network" UI
}Important: Don't pre-check network before requests - race condition.
Critical Anti-Patterns
1. Silent Error Swallowing
// DANGEROUS
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return } // Error ignored!
}
// CORRECT
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error { handleError(error); return }
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
handleHTTPError(response); return
}
guard let data = data else { handleNoData(); return }
}2. Missing Status Code Validation
// DANGEROUS: Assumes nil error means success
let (data, _) = try await URLSession.shared.data(for: request)
return data // Could be 404 error page!
// CORRECT
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError
}3. Retrying Non-Retryable Errors
// DANGEROUS: Retrying 401 won't help
for _ in 0..<3 {
do { return try await fetch() }
catch { continue } // Retries ALL errors
}
// CORRECT
catch let error as URLError where error.code.isRetryable {
continue // Only retry network issues
}4. Blocking Retry Without Backoff
// DANGEROUS: Hammers server
while true {
do { return try await fetch() }
catch { continue } // Immediate retry
}
// CORRECT: Exponential backoff
for attempt in 0..<maxRetries {
do { return try await fetch() }
catch {
let delay = baseDelay * pow(2.0, Double(attempt))
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
}5. Technical Errors to Users
// DANGEROUS
showAlert(error.localizedDescription)
// "Error Domain=NSURLErrorDomain Code=-1004..."
// CORRECT
showAlert(userFriendlyMessage(for: error))
// "Unable to connect. Please check your internet."6. Ignoring Cancellation
// DANGEROUS: Shows error for user cancel
catch { showError(error) }
// CORRECT
catch let error as URLError where error.code == .cancelled {
return // Silent - user initiated
}
catch { showError(error) }Review Questions
Error Handling
- [ ] Are both transport errors and HTTP status codes handled?
- [ ] Is there a centralized error handling strategy?
- [ ] Are error types mapped to user-friendly messages?
Response Validation
- [ ] Is response cast to HTTPURLResponse?
- [ ] Are non-2xx status codes treated as errors?
- [ ] Are error response bodies parsed for messages?
Retry Logic
- [ ] Are only appropriate errors retried (not 4xx)?
- [ ] Is exponential backoff with jitter implemented?
- [ ] Is there a maximum retry count?
- [ ] Is Retry-After header respected for 429/503?
User Experience
- [ ] Are cancellation errors handled silently?
- [ ] Is there a retry option for recoverable errors?
- [ ] Are authentication errors handled separately?
URLRequest Building Reference
Quick Reference
URLRequest Configuration
| Property | Type | Default | Description |
|---|---|---|---|
url | URL? | nil | Request URL |
httpMethod | String? | "GET" | HTTP method |
httpBody | Data? | nil | Request body |
timeoutInterval | TimeInterval | 60.0 | Timeout in seconds |
cachePolicy | CachePolicy | .useProtocolCachePolicy | Cache behavior |
Cache Policies
| Policy | Use Case |
|---|---|
.useProtocolCachePolicy | Default; respects server headers |
.reloadIgnoringLocalCacheData | Always fetch fresh |
.returnCacheDataElseLoad | Offline-first apps |
.returnCacheDataDontLoad | Strictly offline |
Content-Types
| Content Type | Use Case |
|---|---|
application/json | JSON body |
application/x-www-form-urlencoded | Form data |
multipart/form-data; boundary=xxx | File uploads |
HTTP Headers
// Set headers
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// Set all at once
request.allHTTPHeaderFields = [
"Content-Type": "application/json",
"Accept": "application/json"
]Body Encoding
JSON (Recommended)
struct CreateUserRequest: Encodable {
let name: String
let email: String
}
let body = CreateUserRequest(name: "John", email: "john@example.com")
request.httpBody = try JSONEncoder().encode(body)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")Form URL Encoded
// CORRECT: Use URLComponents for proper encoding
var components = URLComponents()
components.queryItems = [
URLQueryItem(name: "username", value: "john"),
URLQueryItem(name: "password", value: "secret")
]
// percentEncodedQuery encodes spaces as + and handles reserved characters
request.httpBody = components.percentEncodedQuery?.data(using: .utf8)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")Warning: Don't use.urlQueryAllowedfor form-encoded values. It includes reserved characters (&,=,+,/,?) that must be escaped in parameter values. UseURLComponentsor a custom charset with only RFC 3986 unreserved characters.
Multipart Form Data
let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var body = Data()
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!)
body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
body.append(imageData)
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = bodyURL Query Parameters
// CORRECT: Use URLComponents
var components = URLComponents(string: "https://api.example.com/search")!
components.queryItems = [
URLQueryItem(name: "query", value: "swift programming"),
URLQueryItem(name: "page", value: "1")
]
let request = URLRequest(url: components.url!)
// Handle plus signs (not encoded by default)
let encodedValue = value?.replacingOccurrences(of: "+", with: "%2B")Timeout Configuration
// Request-level
var request = URLRequest(url: url)
request.timeoutInterval = 30.0
// Session-level
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0 // Resets on each packet
config.timeoutIntervalForResource = 300.0 // Total time (default: 7 days!)Note: Per-requesttimeoutIntervalonly takes effect if it's not more restrictive than the session'stimeoutIntervalForRequest. If the session enforces a stricter limit, that limit applies instead.
Critical Anti-Patterns
1. CRLF Injection (CVE-2022-3918)
Note: This vulnerability affects swift-corelibs-foundation versions before 5.7.3. In 5.7.3+, URLRequest rejects CR/LF in header values at the framework level. Manual sanitization is only needed for projects that cannot upgrade.
// DANGEROUS: User input in headers (affects swift-corelibs-foundation < 5.7.3)
let userInput = "value\r\nEvil-Header: injected"
request.setValue(userInput, forHTTPHeaderField: "X-Custom")
// SAFE: Sanitize header values (for pre-5.7.3 or as defense-in-depth)
let sanitized = userInput.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
request.setValue(sanitized, forHTTPHeaderField: "X-Custom")2. Hardcoded Secrets
// DANGEROUS
request.setValue("sk_live_abc123xyz", forHTTPHeaderField: "Authorization")
// SAFE: From Keychain
let token = KeychainService.shared.getAPIToken()
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")3. Content-Type/Body Mismatch
// BUG: JSON body but wrong Content-Type
request.httpBody = try JSONEncoder().encode(user)
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
// CORRECT
request.httpBody = try JSONEncoder().encode(user)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")4. Manual URL Concatenation
// DANGEROUS: Injection risk
let url = URL(string: "https://api.com/search?q=\(userQuery)")!
// SAFE: URLComponents
var components = URLComponents(string: "https://api.com/search")!
components.queryItems = [URLQueryItem(name: "q", value: userQuery)]5. Memory Issues with Large Files
// DANGEROUS: Loads entire file into memory
let largeFileData = try Data(contentsOf: largeFileURL)
request.httpBody = largeFileData
// SAFE: Use file-based upload
session.uploadTask(with: request, fromFile: largeFileURL)6. Creating Sessions Per Request
// INEFFICIENT
func makeRequest() {
let session = URLSession(configuration: .default) // New each time!
session.dataTask(with: request).resume()
}
// EFFICIENT: Reuse session
class NetworkManager {
private let session = URLSession(configuration: .default)
}Review Questions
Security
- [ ] Are header values sanitized for CRLF characters?
- [ ] Are secrets from Keychain, not hardcoded?
- [ ] Is SSL/TLS validation proper (no blanket trust)?
- [ ] Are credentials excluded from URLs and logs?
Correctness
- [ ] Does Content-Type match body encoding?
- [ ] Is HTTP method appropriate (no body on GET)?
- [ ] Are query parameters built with URLComponents?
- [ ] Are special characters (+, ;, ,) encoded correctly?
Performance
- [ ] Is URLSession reused across requests?
- [ ] Are timeouts configured appropriately?
- [ ] Are large uploads using file-based API?
- [ ] Is
timeoutIntervalForResourceset (not 7-day default)?