Repository navigation
Reuse HTTPS connections, negotiate HTTP/2, add new response fields - #4
Merged
Merged
Conversation
IPDataClient already sends every request through one URLSession (the process-wide URLSession.shared by default), so connections are pooled and HTTP/2 is negotiated natively on Apple platforms and on Linux (libcurl). This commit makes that behaviour explicit and complete: - Add `init(apiKey:baseURL:configuration:)`, which creates a dedicated URLSession once from the given configuration, owns it for the lifetime of the client and calls `finishTasksAndInvalidate()` in `deinit`. The existing `init(apiKey:baseURL:session:)` keeps `.shared` as its default and never invalidates an injected session; both public initialisers now delegate to one private designated initialiser. - Send `Accept: application/json` next to the existing `api-key` header on every request. - Retry `POST /bulk` exactly once when the transport fails with `URLError.networkConnectionLost`, the error a pooled connection produces when the server closed it while idle. Nothing else is retried, and HTTP statuses are never retried. - Keep the Linux `URLSession.data(for:)` shim unchanged. Tests: consecutive lookups of every kind arrive through the single injected mock session (handler invocations counted); the owned-session initialiser routes requests through its configuration, honours `baseURL` and lets the client deallocate; the Accept header is asserted on GET, raw-field and bulk requests; the bulk retry happens once and only for `networkConnectionLost`, never for other errors or HTTP statuses, and plain lookups are not retried by the client. The bulk test now reads the request body from `httpBodyStream` when `httpBody` is nil, which is how Apple platforms hand POST bodies to a URLProtocol; this fixes the macOS test crash on main that PR #3 also addresses, in a form that compiles on Linux too. README: new "Connection reuse and HTTP/2" section after Quick Start, and the install snippet points at 1.0.2, the release that ships these changes. Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The API now returns two more top-level booleans and three more keys per blocklist entry: - `IPResponse.isSatellite` and `IPResponse.isAnycast` (`is_satellite`, `is_anycast`), siblings of `isEU`. The API includes each key only when it is true, so they are plain `Bool`s decoded with `decodeIfPresent(Bool.self, ...) ?? false` in the existing custom `init(from:)`: absent or null reads false. - `Blocklist.firstSeen`, `Blocklist.lastSeen` (`first_seen`, `last_seen`, `YYYY-MM-DD` UTC strings) and `Blocklist.daysSeen` (`days_seen`, Int). Some feeds, for example ipdata's own vpn feed, omit all three, so they are optional and decode to nil when absent. The dates stay strings: no date parsing, no new dependencies. `JSONDecoder` ignores unknown keys by default and the client configures nothing else, so responses with further new keys keep decoding; a test now pins that. The client does not validate `fields=` names, so the new names pass through unchanged (covered by a client test). Tests: fixtures built from the three sample responses (satellite ISP; anycast address with dated blocklist entries; VPN exit whose third blocklist entry has no dates), the same shape in a bulk response, false-when-absent on the existing fixtures, explicit false/true/null values, and unknown keys. README: the two flags join the IPResponse table, a Blocklist table documents the entry fields and which feeds omit the dates, and the usage examples show both. Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two logical commits:
018b4dfReuse HTTPS connections and retry lost-connection bulk lookups.eba3de3Add is_satellite, is_anycast and blocklist date fields.IPDataClientalready sent every request through a singleURLSession(URLSession.sharedby default), so connection pooling and HTTP/2 were structurally in place on Apple platforms and on Linux (swift-corelibs-foundation on libcurl withCURLPIPE_MULTIPLEX). This PR makes that behaviour explicit and complete:public convenience init(apiKey:baseURL:configuration: URLSessionConfiguration)creates an ownedURLSessiononce and callsfinishTasksAndInvalidate()on it indeinit. The existinginit(apiKey:baseURL:session:)keepsURLSession.sharedas its default (a dedicatedURLSessionper client would regress callers that instantiate a client per call) and never invalidates an injectedURLSession. Both public initialisers now delegate to one private designated initialiser;IPDataClientisfinal, so turning the public designated initialiser into a convenience one is source compatible.Accept: application/jsonis set next to the existingapi-keyheader on every request (GET lookups, raw single-field lookups, bulk POST).POST /bulkis retried exactly once when the transport fails withURLError.networkConnectionLost. Nothing else is retried, HTTP statuses are never retried, and the retry goes through the sameURLSession(so a fresh pooled connection).URLSession.data(for:)shim is unchanged.IPResponse.isSatelliteandIPResponse.isAnycast(Bool, decoded withdecodeIfPresent(Bool.self, ...) ?? falsein the existing custominit(from:)),Blocklist.firstSeen/Blocklist.lastSeen(String?, kept as theYYYY-MM-DDUTC strings) andBlocklist.daysSeen(Int?).JSONDecoderignores unknown keys by default and the client configures nothing else; a test pins that.MockURLProtocolseam; none calls the live API): six lookups of every kind on one client all reach the mock protocol registered only on the injectedURLSession; theconfiguration:initialiser routes requests through its configuration, honoursbaseURL, and the client deallocates (runningdeinit, which invalidates the ownedURLSession);Accept: application/jsonis asserted on GET, raw-field and bulk requests; the bulk retry happens once and only once, not for other network errors, not on an HTTP status, andlookupis not retried by the client; the three API sample responses decode (satellite, anycast with dated blocklists, blocklist entry without dates), the new flags read false unless sent as true, bulk responses carry the new fields, unknown keys are ignored, andfields=passes the new names through unchanged (the client validates no field names).1.0.2, the release that ships these changes.CIworkflow already runs onpull_requesttomain.Overlap with open PR #3. On
mainthe macOS CI job fails becausetestBulkLookupSendsPostWithIPsInBodyforce-unwrapsrequest.httpBody, which Apple platforms hand to aURLProtocolashttpBodyStream(every CI run on this repository so far is red for that reason on macOS). PR #3 fixes the same test; its CI run has macOS green but the Ubuntu 5.10 job failing (5.9 cancelled by fail-fast): its fallback branch constructsHTTPURLResponse(), andHTTPURLResponsehas no accessible parameterless initialiser in swift-corelibs-foundation. This PR has to touch that test anyway (to assert theAcceptheader), so it reads the body through a small file-private helper (httpBody, elsehttpBodyStream) that compiles on both platforms and throws instead of fabricating a placeholder response. If PR #3 merges first, the conflict is confined to those lines and either version can be kept.What changes for users
IPDataClient(apiKey:)still usesURLSession.shared; every existing initialiser, method and error type keeps its signature. Changes are additive only.IPDataClient(apiKey:baseURL:configuration:)for a client that owns a dedicatedURLSession(custom timeouts, proxies, isolation from the shared pool). It is invalidated automatically when the client is deallocated; there is nothing to close by hand, andURLSession.sharedis never invalidated.URLSessionexposes no pool idle timeout, HTTP/2 keepalive ping or TCP keepalive setting (timeoutIntervalForRequestis per request), so the platform defaults apply.Accept: application/json.bulkLookuptransparently resends its POST once if a pooled connection was closed by the server before any response arrived (URLError.networkConnectionLost). The retry keys on that error alone, becauseURLSession.data(for:)cannot tell whether response bytes had already arrived. Other errors, and HTTP statuses, surface exactly as before.IPResponse.isSatellite,IPResponse.isAnycast(Bool,falsewhen the API omits the key),Blocklist.firstSeen,Blocklist.lastSeen(String?),Blocklist.daysSeen(Int?). Neither type has a public memberwise initialiser (values come from decoding), so adding stored properties breaks no caller. Encoding anIPResponsenow always writesis_satelliteandis_anycast.URLSessionor configuration.Release notes
Version
1.0.2. After merging, tag the merge commitv1.0.2and publish a GitHub release with:Verification
CI, run 41621101180 oneba3de3(eventpull_request):Swift 5.9 on Ubuntusuccess,Swift 5.10 on Ubuntusuccess,Swift on macOSsuccess. On every jobswift testexecuted 40 tests with 0 failures (25 inIPDataClientTests, 15 inIPDataTests). This is the first fully green run in the repository (all five earlier runs were red on at least one job). No follow-up commit was needed.Noticed, not changed
User-Agentheader is added: a Swift package carries no version metadata to deriveipdata-swift/<version>from, and a hard-coded value would go stale.bulkLookupcontains dead code:if !url.absoluteString.contains("api-key")can never be true becausebuildURLalways adds theapi-keyquery item.IPResponsetable still liststimeZoneasTimeZone?although the type was renamedTimeZoneInfoin Rename TimeZone to TimeZoneInfo to fix macOS CI failure #2, and the table has no rows forregionTypeandemojiUnicode.macos-14) was not modernised, and theswift-tools-versionstays 5.9.MockURLProtocoluses mutable statics (requestHandler, and nowrequestCount), fine in the Swift 5 language mode the package uses; they would need isolation under Swift 6 strict concurrency.URLSession.data(for:)shim would become ambiguous if a future swift-corelibs-foundation ships the same async method; the 5.9 and 5.10 images used by CI compile it today.🤖 Generated with Claude Code