Repository navigation
Reuse HTTPS connections, negotiate HTTP/2, add new response fields - #37
Merged
Merged
Conversation
Every IPDataClient created without an HttpClient used to build its own HttpClient, so each client paid a fresh TCP and TLS handshake, and the netstandard2.0-only assembly never negotiated HTTP/2. * Target netstandard2.0 and net8.0. System.Text.Json is referenced only for netstandard2.0; it ships in the shared framework on net8.0. * HttpClientAdapter: default adapters share one process-wide HttpClient built lazily by CreateDefaultClient(). On .NET 5+ it wraps a SocketsHttpHandler with a 5 minute connection lifetime, a 60 s idle timeout, HTTP/2 keep-alive pings every 30 s (15 s timeout, policy Always) and cookies disabled; on .NET Standard 2.0 an HttpClientHandler with cookies disabled. The shared client is never disposed (process lifetime). User-supplied HttpClient/IHttpClient instances stay user-owned. The wrapped client is exposed internally for tests. * IPDataClient: all eleven requests are built by CreateRequest(), which on .NET 5+ asks for HTTP/2 with HttpVersionPolicy.RequestVersionOrLower so the handler falls back to HTTP/1.1 when h2 is not negotiated. The netstandard2.0 asset never sets Version 2.0 (.NET Framework throws). SendRequestAsync disposes the request and the response. New additive constructor IPDataClient(string, HttpClient, Uri) combines a caller-owned HttpClient with a custom base URL. * JsonSerializer: IgnoreNullValues is obsolete on net8.0, so that build uses DefaultIgnoreCondition = WhenWritingNull instead. * Tests: request version and policy are asserted for GET and bulk POST through the existing Mock<IHttpClient>; a success-path test proves disposal does not break deserialisation; HttpClientAdapterTests cover the shared default client, pass-through of a custom client and the default handler configuration; the real-network test is removed. A RecordingHttpMessageHandler drives a real HttpClient offline. * README: new "Connection reuse and HTTP/2" section. CI: pack step so multi-TFM packaging failures surface on pull requests. * VersionPrefix 3.0.1 -> 3.1.0 (publish derives the version from the tag). 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 optional
fields on every threat.blocklists entry.
* IPLookupResult gains bool IsSatellite ("is_satellite") and bool
IsAnycast ("is_anycast") next to IsEu. The API includes each key only
when it is true, so both read false when the key is absent. Because
they carry [JsonPropertyName], x => x.IsSatellite / x => x.IsAnycast
work as field selectors for single-field and fields= lookups.
* BlocklistInfo gains string FirstSeen ("first_seen"), string LastSeen
("last_seen", both YYYY-MM-DD UTC, kept as strings) and int? DaysSeen
("days_seen"). Some feeds omit all three, so they are nullable.
* Threat.Blocklists now has a public setter: System.Text.Json does not
populate collection properties with a non-public setter, so since the
move from Newtonsoft.Json the list always deserialised empty and the
new fields would have been unreachable.
* System.Text.Json already ignores unknown members (is_vpn, scores and
future keys); a test pins that.
* Tests: fixtures built from the three spec samples (satellite ISP,
anycast address with dated blocklist entries, VPN exit with an undated
entry) assert the typed values, the false-when-absent behaviour, the
per-item shape of bulk responses and that the two new names are
accepted as selectable fields.
* README: "Response fields" section documenting the five names.
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
Every
IPDataClientcreated without anHttpClientused to build its ownHttpClient, so each client paid a fresh TCP + TLS handshake, and the netstandard2.0-only assembly never negotiated HTTP/2. This PR makes the library amortise the cost of HTTPS and adds the response fields the API now returns. Two commits:1. Reuse HTTPS connections and negotiate HTTP/2
IPData.csprojtargetsnetstandard2.0;net8.0;System.Text.Json4.7.2 is referenced only for netstandard2.0 (in-box on net8.0).VersionPrefix3.0.1 → 3.1.0.HttpClientAdapter: default adapters share one process-wideHttpClientcreated lazily byCreateDefaultClient()(Lazy<HttpClient>, never disposed). On .NET 5+ it wraps aSocketsHttpHandlerwithPooledConnectionLifetime5 min,PooledConnectionIdleTimeout60 s,KeepAlivePingDelay30 s,KeepAlivePingTimeout15 s,KeepAlivePingPolicy.Always,UseCookies = false; on .NET Standard 2.0 anHttpClientHandler { UseCookies = false }. User-suppliedHttpClient/IHttpClientinstances stay user-owned.internal HttpClient Clientis exposed for tests.IPDataClient: all eleven requests go through a privateCreateRequest(HttpMethod, Uri); on .NET 5+ it setsVersion = HttpVersion.Version20andVersionPolicy = RequestVersionOrLower, so HTTP/2 is negotiated through ALPN with automatic HTTP/1.1 fallback. The netstandard2.0 asset never sets Version 2.0 (.NET Framework throws).SendRequestAsyncdisposes the request and the response. New additive constructorIPDataClient(string apiKey, HttpClient httpClient, Uri baseUrl).JsonSerializer:IgnoreNullValuesis obsolete on net8.0 and is replaced there byDefaultIgnoreCondition = WhenWritingNull.Mock<IHttpClient>; a success-path test proves disposal does not break deserialisation;HttpClientAdapterTestscover the shared default client, custom-client pass-through and the handler configuration (the default client's handler is reached through reflection onHttpMessageInvoker._handler, as there is no public accessor; the helper fails with an explicit message if that field is ever renamed); the real-network test is deleted; aRecordingHttpMessageHandlerdrives a realHttpClientoffline.dotnet pack … --no-buildsmoke step so multi-TFM packaging failures surface on PRs.2. Add is_satellite, is_anycast and dated blocklist entries
IPLookupResult.IsSatellite/IsAnycast(bool,[JsonPropertyName("is_satellite")]/("is_anycast")),falsewhen the key is absent; usable as field selectors.BlocklistInfo.FirstSeen/LastSeen(string,YYYY-MM-DDUTC) andDaysSeen(int?), all optional.Threat.Blocklistssetter made public. System.Text.Json never populates collection properties without a public setter, so since the Newtonsoft.Json → System.Text.Json migration the list has always deserialised empty (on master, the API's complete sample response givesThreat.Blocklists.Count == 0), and the newfirst_seen/last_seen/days_seenfields would have been unreachable.IPLookupResult.Languageshas the identical defect but is outside the lines this change touches (see "Noticed, not changed").What changes for users
new IPDataClient("API_KEY")now shares one process-wide pooledHttpClientwith every other default-constructed client; consecutive lookups reuse the TCP/TLS connection instead of handshaking each time. Keep one client (or one per API key / endpoint) and reuse it for every call.net8.0asset: every request asks for HTTP/2 and falls back to HTTP/1.1 automatically. .NET Framework and other .NET Standard 2.0 consumers get thenetstandard2.0asset with HTTP/1.1 keep-alive, now over a shared pool.IPDataClient(string apiKey, HttpClient httpClient, Uri baseUrl)to combine your ownHttpClient(for example fromIHttpClientFactory) with the EU endpoint.HttpClient/IHttpClientyou pass in remains yours to dispose.IPDataClientholds no per-request state and is safe to use concurrently.POST /bulk.SocketsHttpHandleron .NET 8 discards pooled HTTP/1.1 connections the server has already closed before reusing them, retries HTTP/2 streams the server rejected via GOAWAY or that failed before being sent, and retries an HTTP/1.1 request whose reused connection returned EOF before any response byte only when no request body was sent (request.Content is null, orExpect: 100-continuewas still pending). APOST /bulkwhose body was already written to a connection that then dies is therefore not retried by the stack and surfaces as an exception; the library adds no retry of its own, because from outsideHttpClientit cannot tell "no response byte arrived" apart from "the body read failed".IPLookupResult.IsSatellite,IPLookupResult.IsAnycast(falsewhen the API omits the key) andBlocklistInfo.FirstSeen,LastSeen,DaysSeen(nullwhen a feed omits them).Threat.Blocklistsis now actually populated from the response.Threat.Blocklistsgains a public setter. The library now disposes eachHttpRequestMessageandHttpResponseMessageafter reading the body, so a customIHttpClientmust not hold on to the request it receives.Release notes
v3.1.0 — tag to push after merge:
v3.1.0(the "Publish to NuGet" workflow derives the package version from the tag).Connection reuse and HTTP/2
netstandard2.0andnet8.0assets.IPDataClientinstances created without anHttpClientshare one process-wide, pooledHttpClient: consecutive lookups reuse the HTTPS connection instead of paying a TCP + TLS handshake per call. Create one client per process (or per API key / endpoint) and reuse it; there is nothing to dispose.IPDataClient(string apiKey, HttpClient httpClient, Uri baseUrl).New response fields
IPLookupResult.IsSatellite(is_satellite) andIPLookupResult.IsAnycast(is_anycast):bool,falsewhen the API omits the key; both usable as field selectors (x => x.IsSatellite).BlocklistInfo.FirstSeen(first_seen) andBlocklistInfo.LastSeen(last_seen):YYYY-MM-DDUTC strings;BlocklistInfo.DaysSeen(days_seen):int?. Allnullwhen the feed does not report them.Fixes
Threat.Blocklistsis populated again; it deserialised as an empty list since the migration to System.Text.Json.Verification
All commands were run from the repository root with .NET SDK 8.0.133 before each push:
Test counts: 100 on master → 112 after commit 1 (+5 adapter tests, +7 client tests, the real-network test deleted) → 121 after commit 2 (+6 serializer tests, +3 field-selection tests).
Two requests from an
IPDataClientto api.ipdata.co were served over HTTP/2 (response.Version=2.0) and the second reused the connection.CI: the
CIworkflow (restore, build Release, test, pack) already has apull_requesttrigger for master. Run #16 on 9d45708 (pull_request, jobbuild-and-test): success — Restore, Build, Test (121 passed) and the new Pack step all passed. BecauseGeneratePackageOnBuildis on, the Build step already packs and the Pack step is an incremental no-op on this run; it still fails the job if packaging ever breaks or that property is removed.Noticed, not changed
IPLookupResult.Languageshas the same private-setter defect thatThreat.Blocklistshad: System.Text.Json never populates it, so it is always empty. One-line fix ({ get; set; }), left out because it is outside this change's scope.Threathas no properties foris_vpnandscores(vpn_score,proxy_score,threat_score,trust_score), which the API now returns (both appear in its sample responses); they are silently ignored.User-Agentheader and noAccept: application/jsonheader; the request path sets no headers at all.HttpClientHandler,IgnoreNullValues) are build-verified but have no runnable tests.src/IPData/IPData.nuspecis stale and unused bydotnet pack(SDK-style project): it lists only a .NETStandard2.0 dependency group and different authors.PackageIconUrlis deprecated (NU5048);PackageIconwith an embedded icon would silence it.ApplicationIcon/Win32Resourceare set on a class library.ApiExceptionand its subclasses keep[Serializable]andSerializationInfoconstructors, which produce SYSLIB0051 obsolete warnings on net8.0.IPData.slnlists.github/CODE_OF_CONDUCT.mdand.github/PULL_REQUEST_TEMPLATE.mdas solution items, but neither file exists..editorconfigasks for CRLF in*.cs, but about half of the files are LF; this PR keeps each file's existing line endings.🤖 Generated with Claude Code