ios-networking — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-networking (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
.useProtocolCachePolicy as the default cache policy. Configure cache size explicitly for production apps.| Task | Solution | Reference |
|---|---|---|
| Simple GET/POST | URLSession.shared.data(for:) | urlsession.md |
| Multiple endpoints | Generic APIClient + Endpoint protocol | api-client.md |
| Auth with token refresh | Actor-based TokenManager | error-retry.md |
| Real-time data | URLSessionWebSocketTask | advanced.md |
| Large file download | URLSession download task | urlsession.md |
| Background upload | Background URLSession configuration | urlsession.md |
| Offline support | URLCache + .returnCacheDataElseLoad | advanced.md |
| Network status | NWPathMonitor | advanced.md |
| File upload | MultipartFormData builder | advanced.md |
| Dynamic JSON | JSONValue enum | api-client.md |
| Certificate pinning | URLSessionDelegate | advanced.md |
| GraphQL | Apollo iOS 2.0 | advanced.md |
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
throw NetworkError.httpError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(User.self, from: data)
}// 1. Define endpoint
struct GetUserEndpoint: Endpoint {
typealias Response = User
let userId: Int
var path: String { "/users/\(userId)" }
var method: HTTPMethod { .get }
}
// 2. Call through client
let user = try await apiClient.send(GetUserEndpoint(userId: 42))Request → AuthInterceptor (attach token)
→ URLSession.data(for:)
→ 401? → TokenManager.forceRefresh()
→ Retry original request with new token
→ Decode response| Mistake | Why It's Wrong | Fix |
|---|---|---|
URLSession() per request | Kills HTTP/2 multiplexing, leaks memory | Reuse a shared or injected session |
| Checking reachability before request | Race condition, wastes time | Just make the request, handle errors |
NSAllowsArbitraryLoads = true | Disables all ATS security | Use domain-specific exceptions |
| Decoding on main thread | Blocks UI for large payloads | URLSession already decodes off-main |
| Force-unwrapping URL | Crashes on malformed strings | Use guard + throw pattern |
| Ignoring HTTP status codes | 404/500 treated as success | Always validate response status |
| Mocking URLSession directly | Fragile, doesn't test serialization | Use URLProtocol subclass |
JSONSerialization for Codable types | Verbose, error-prone | Use JSONDecoder/JSONEncoder |
| Retry without backoff | Server overload, ban risk | Exponential backoff + jitter |
| Token refresh without dedup | Multiple simultaneous refreshes | Actor with stored Task |
Need to upload?
├── Small file (<5MB) → URLSession upload task with Data
├── Large file (>5MB) → URLSession upload task with file URL
├── Multiple files → MultipartFormData builder
├── Background upload → Background URLSession config
└── Progress tracking → URLSessionTaskDelegate (async delegate)| Method | Idempotent | Body | Use Case |
|---|---|---|---|
| GET | Yes | No | Fetch resource |
| POST | No | Yes | Create resource |
| PUT | Yes | Yes | Replace resource |
| PATCH | No | Yes | Partial update |
| DELETE | Yes | Optional | Remove resource |
| HEAD | Yes | No | Check existence |
// URLProtocol mock setup
final class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
guard let handler = Self.requestHandler else {
client?.urlProtocolDidFinishLoading(self)
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
// Usage in tests
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
let client = APIClient(session: session)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = try JSONEncoder().encode(User(id: 1, name: "Test"))
return (response, data)
}
let user = try await client.send(GetUserEndpoint(userId: 1))
XCTAssertEqual(user.name, "Test")httpMaximumConnectionsPerHost (default 6, increase for API-heavy apps)timeoutIntervalForRequest (30s default, reduce for user-facing)waitsForConnectivity = true for non-urgent requestsAsyncBytes for streaming instead of buffering large responses.task modifier in SwiftUI)| Feature | Minimum iOS |
|---|---|
| URLSession async/await | 15.0 |
| URLSession.data(for:) | 15.0 |
| AsyncBytes | 15.0 |
| URLSessionWebSocketTask | 13.0 |
| NWPathMonitor | 12.0 |
| Background URLSession | 7.0 |
| Codable | 11.0 |
| async URLSession delegate | 15.0 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.