Repository navigation
Reuse HTTPS connections, negotiate HTTP/2, add new response fields - #11
Merged
Merged
Conversation
The client already holds one PSR-18 client for its whole lifetime, so the persistent connections of the injected transport are reused across lookups. Symfony HttpClient with ext-curl negotiates HTTP/2 on its own, but Guzzle and php-http/curl-client only use HTTP/2 when the PSR-7 request carries protocol version 2.0. Add a trailing, optional `?string $httpVersion` constructor argument that a new private createRequest() helper applies through withProtocolVersion() for both lookup() and bulkLookup(); null (the default) keeps today's behaviour, and the lookup()/bulkLookup() signatures are unchanged. While touching the constructor signature, declare the $httpClient and $requestFactory parameters explicitly nullable (?ClientInterface, ?RequestFactoryInterface): PHP 8.4 deprecates implicitly nullable parameters, and the syntax works from PHP 7.1. Tests cover the version being applied to GET and POST requests and left to the request factory when null, consecutive calls going through the same injected client, and an explicit null request factory. The README gains a "Connection reuse and HTTP/2" section (one Ipdata per process, Symfony automatic, Guzzle opt-in, stream clients must stay on null, PHP-FPM versus long-running workers, no multiplexing gain through the synchronous PSR-18 API). CHANGELOG gets the 0.1.3 entry and the missing 0.1.2 entry for PR #9. Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The API now returns two top-level booleans, is_satellite and
is_anycast, which it includes only when true, and three optional keys
on each threat.blocklists entry: first_seen and last_seen (YYYY-MM-DD,
UTC) and days_seen (integer), which some feeds omit. This client
returns the decoded JSON unchanged and does not validate field names,
so no code change is needed.
Add fixtures built from the API samples (satellite ISP, anycast address
with dated blocklist entries, VPN exit with an undated entry) and tests
asserting that lookup() and bulkLookup() pass the values through, that
the flags are simply absent when false, that the dates stay strings,
and that both new names can be requested through $fields. Document the
five fields in the README ("present only when true") and in the 0.1.3
CHANGELOG entry.
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
Part of the change that brings connection reuse, HTTP/2 and the new response fields to all official ipdata client libraries. Two logical commits:
Ipdataalready holds one PSR-18 client for its whole lifetime, so the persistent connections of the injected transport are reused across lookups; the library is transport-agnostic. Symfony HttpClient with ext-curl negotiates HTTP/2 on its own, but Guzzle and php-http/curl-client only use HTTP/2 when the PSR-7 request carries protocol version2.0. The constructor gains a trailing, optional?string $httpVersion = null; a new privatecreateRequest(string $method, string $url): RequestInterfaceapplies it withwithProtocolVersion()at both request sites (lookup()andbulkLookup()keep their signatures, so the phpstan baseline stays valid). While touching the signature, parameters 2 and 3 are declared explicitly nullable (?ClientInterface,?RequestFactoryInterface) to avoid the PHP 8.4 implicit-nullable deprecation (PHP 7.1+ syntax). Tests, README section "Connection reuse and HTTP/2", CHANGELOG0.1.3plus the backfilled0.1.2entry for Add EU endpoint support and improve API client functionality #9.is_satelliteandis_anycast(top level, present only whentrue) andfirst_seen,last_seen(YYYY-MM-DD, UTC) anddays_seenonthreat.blocklistsentries (absent for some feeds). The client returns the decoded JSON unchanged and does not validate field names, so no code change was needed: fixtures built from the API samples (tests/Fixtures/*.json), tests forlookup()andbulkLookup(), README "Available Fields" documentation and the CHANGELOG bullet.No CI changes:
ci.ymlalready runs onpull_request.What changes for users
$httpVersion. Pass'2.0'with Guzzle or php-http/curl-client to negotiate HTTP/2 (curl falls back to HTTP/1.1 if the server does not offer h2). Leave itnull(default) with Symfony'sPsr18Client, which negotiates HTTP/2 itself and ignores the PSR-7 version by default, and with stream-based clients, which would writeHTTP/2.0literally into the request line. BecauseIpdata::DEFAULT_BASE_URLis private, the positional form repeats the base URL:new Ipdata($key, new GuzzleHttp\Client(), new Psr17Factory(), 'https://api.ipdata.co', '2.0'); PHP 8 users can writehttpVersion: '2.0'.$httpClientand$requestFactoryare explicitly nullable: same behaviour, no deprecation notice on PHP 8.4.Ipdata(and one PSR-18 client) per process and register it as a service; under PHP-FPM/mod_php reuse spans the lookups of one PHP request, long-running workers benefit fully; the synchronous PSR-18 API gains nothing from multiplexing, the saving is the TLS handshake; noclose()because the connections belong to the injected HTTP client.is_satellite/is_anycastare present only whentrue(read$data['is_anycast'] ?? false); blocklist entries may carryfirst_seen,last_seen,days_seen, which some feeds omit ($entry['first_seen'] ?? null). Both flag names can be requested through the$fieldsargument.Release notes
Version 0.1.3. After merging, push tag
0.1.3on the merge commit (git tag 0.1.3 && git push origin 0.1.3); the Packagist hook publishesipdata/api-client0.1.3.The CHANGELOG now also carries the previously missing
0.1.2entry (caller IP lookup, EU endpoint,bulkLookup(), bulkContent-Typefix, removed unused dev dependencies), reconstructed from #9.Verification
Environment: PHP 8.3.6 CLI (ext-curl 8.5.0, OpenSSL 3.0.13, HTTP/2 capable), PHPUnit 8.5.56, Composer with
composer installof the repository'srequire-dev.vendor/bin/phpunit: baseline on masterOK (17 tests, 38 assertions); after commit 1OK (22 tests, 54 assertions); after commit 2 (pushed state, run again right before the push)OK (27 tests, 96 assertions).PhpParser\Lexer::getNextToken(): Return value must be of type int, null returned, and Psalm 4 does not support PHP 8.2+), so the checks were run with the newest compatible releases against the repository's own configuration:phpstan.neon.dist(level 6 +phpstan-baseline.neon): no errors in the code; two "Ignored error pattern ... was not matched" reports for the baseline entries aboutbulkLookup()/buildLookup()$ips. Identical on the pristine master tree (checked withgit stash), so not introduced here.psalm.xml(errorLevel 2):No errors found!PHP_CS_FIXER_IGNORE_ENV=1, repository.php_cs, pathssrc tests) and PHP-CS-Fixer 3.95.27 with an equivalent@Symfony+ short array syntax config: both report only the two pre-existing hits insrc/Ipdata.php(yoda_styleon$ip !== '',phpdoc_annotation_without_doton@deprecated Use bulkLookup() instead.), identical before and after both commits; nothing on changed lines.composer-normalize --dry-run:composer.jsonis not normalized today (key order); this PR does not touch it.symfony/http-client7.4.22 andnyholm/psr7, oneIpdatawith oneSymfony\Component\HttpClient\Psr18Client, a dummy API key (no real key used anywhere), two sequentiallookup()calls, curl transfer info read from the Symfony responses:GET /8.8.8.8: 401 "You have not provided a valid API Key.",http_version=3(CURL_HTTP_VERSION_2_0, i.e. HTTP/2), curl logALPN: curl offers h2,http/1.1/ALPN: server accepted h2/using HTTP/2, total 0.458 s.GET /1.1.1.1: 401, HTTP/2, curl logRe-using existing connection,connect_time=0.0000s, total 0.066 s. Both requests went through the same PSR-18 client instance. The same result with$httpVersion = '2.0', confirming Symfony'sPsr18Clientignores the PSR-7 version by default.http_version=2 status=401 num_connects=1, thennum_connects=0for a second URL in the same invocation.CONNECT tunnelto127.0.0.1); the server's own h2 support was verified by the spec author on 2026-11-02 (curl from there: HTTP/2, 401, second request reused the connection with 0 new connects).2.0toCURLOPT_HTTP_VERSION = CURL_HTTP_VERSION_2_0comes from their source), and the pinned analyzer versions on PHP 8.0.CIworkflow (pull_requesttrigger: "Build lowest version" on PHP 7.2 with--prefer-lowest, "Build" on PHP 7.2/7.3/7.4/8.0) and theStatic analysisworkflow run on this PR; see the Checks tab. Status at the time this description was last updated (2026-11-03 10:45 UTC):CIrun #22 (https://github.com/ipdata/php/actions/runs/41620684185) success, all five jobs green (Build lowest version, Build 7.2, 7.3, 7.4, 8.0), each loggingOK (27 tests, 96 assertions);Static analysisrun #19 (https://github.com/ipdata/php/actions/runs/41620684422) queued: its four jobs request the retiredubuntu-20.04runner label, no runner picks them up, and GitHub cancels such runs after 24 h (exactly what happened to runs #17 and #18). The equivalent checks were run locally, see above.Noticed, not changed
.github/workflows/static.ymlruns its four jobs onubuntu-20.04, a runner image GitHub has retired: the workflow's recent runs (#17 on Add EU endpoint support and improve API client functionality #9, #18 on Dependabot Bump shivammathur/setup-php from 1.7.0 to 2.37.1 in /.github/workflows #10) ended ascancelledafter queuing for 24 h, so PHPStan, Psalm, PHP-CS-Fixer and composer-normalize do not actually run on pull requests today. The pinned tools are also old (PHPStan 0.12.88, Psalm 4.7.2, PHP-CS-Fixer 2.19.0 on PHP 8.0)..github/workflows/ci.ymlhas a stale matrix: PHP 7.2 to 8.0 only,shivammathur/setup-php@1.7.0,actions/checkout@v2,tools: prestissimo(a Composer 1 plugin). Dependabot Bump shivammathur/setup-php from 1.7.0 to 2.37.1 in /.github/workflows #10 (setup-php 2.37.1) currently fails CI.src/Ipdata.phpon lines this PR does not touch:$url = $ip !== '' ? ...(yoda_style) and@deprecated Use bulkLookup() instead.(phpdoc_annotation_without_dot).phpstan-baseline.neon(added in Add EU endpoint support and improve API client functionality #9, whose static-analysis run was cancelled) has two entries forbulkLookup()/buildLookup()$ipsthat PHPStan 1.12 reports as unmatched because both methods document@param array<string> $ips; not checked with the pinned 0.12.88.composer.jsonis notcomposer-normalized (thetypekey order), so the Composer Normalize job would fail if it ran..php_cscallsFinder::exclude(__DIR__.'/vendor')with an absolute path;exclude()takes a directory name relative toin(), sovendoris not actually excluded when the fixer runs from the config alone.Ipdata::DEFAULT_BASE_URLis private, so the new fifth argument requires repeating'https://api.ipdata.co'positionally (or a PHP 8 named argument). Making the constant public would be an additive follow-up.User-Agentheader and noAcceptheader; the only header it sets isContent-Typeon the bulk POST.Accept: application/jsonheader was added (R9 permits it only where the request path already sets headers; the PHP section lists the exact code changes and thelookup()path sets no headers, so adding it to the bulk POST alone would be inconsistent); (b) the pinned analyzers were replaced by current releases for local verification only, CI stays as it is; (c) the0.1.2CHANGELOG entry was reconstructed from Add EU endpoint support and improve API client functionality #9's description and commits; (d) the fixtures are JSON files undertests/Fixtures/rather than inline arrays; (e) the README section sits after the last "How to use" subsection ("Bulk request") and before "Available Fields".https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke
🤖 Generated with Claude Code