Skip to content

[+]: PHP 8.0+ rework#26

Merged
voku merged 7 commits intomasterfrom
php8
Apr 30, 2026
Merged

[+]: PHP 8.0+ rework#26
voku merged 7 commits intomasterfrom
php8

Conversation

@voku
Copy link
Copy Markdown
Owner

@voku voku commented Apr 30, 2026

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Added bearer token authentication support
    • Added retry configuration with customizable delays, backoff, and max time limits
    • Added advanced TLS/certificate pinning and CA bundle management
    • Added cookie persistence support
    • Added HTTP/2 and HTTP/3 protocol helpers
    • Added Alt-Svc and HSTS cache controls
    • Added proxy routing and configuration helpers
    • Added response transfer info accessors (effective URL, IPs, timing, redirect count, negotiated HTTP version)
  • Bug Fixes

    • Fixed NTLM authentication option preservation
    • Fixed retry max time budget initialization
    • Improved Content-Type header parsing
  • Breaking Changes

    • Dropped PHP 7.4 support; requires PHP 8.0+

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@Copilot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 46 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58f72ce4-a7a4-4b4a-891b-8f4b3499acb9

📥 Commits

Reviewing files that changed from the base of the PR and between fb29841 and 869b5e0.

📒 Files selected for processing (2)
  • src/Httpful/Request.php
  • src/Httpful/Response.php

Walkthrough

The pull request updates Httpful to require PHP 8.0+ and significantly enhances the request/response API by introducing bearer token authentication, configurable retry logic with exponential backoff, advanced TLS/certificate options, cookie persistence, HTTP/2 and HTTP/3 support, Alt-Svc/HSTS caching, proxy routing, and response transfer-info extraction. Type annotations are strengthened throughout.

Changes

Cohort / File(s) Summary
CI & Build Configuration
.github/workflows/ci.yml, composer.json
Restricts PHP test matrix to 8.0–8.5, removes PHP 7.4 support, makes PHPStan errors fail CI. Upgrades PHP requirement to ^8.0, updates PHPStan to ^2.1, and restricts PHPUnit to ^9.6.
Documentation & Changelog
CHANGELOG.md, README.md, phpstan.neon
Documents new features (bearer auth, retry, TLS, cookies, HTTP/2/3, Alt-Svc/HSTS, proxy), updates PHP 8.0+ requirement, and removes suppressed PHPStan checks.
Type Annotations – Request/Response Builders
src/Httpful/Client.php, src/Httpful/ClientMulti.php, src/Httpful/Factory.php
Refines method signatures with explicit nullable typehints (?array, ?string, ?int) and adds generic docblock types (array<string, mixed>). No logic changes.
Type Annotations – Core Classes
src/Httpful/Headers.php, src/Httpful/Stream.php, src/Httpful/Uri.php, src/Httpful/ServerRequest.php, src/Httpful/UploadedFile.php, src/Httpful/Http.php
Strengthens type annotations with generics and nullable parameters across multiple utility classes. Adds HTTP_3 constant. Minimal logic changes.
Type Annotations – MIME Handlers
src/Httpful/Handlers/*.php
Refines PHPDoc return/parameter types for CSV, Form, JSON, XML, and default MIME handlers with generics (e.g., list<list<string|null>>, array<string, mixed>). No parsing logic changes.
Core cURL Refactoring
src/Httpful/Curl/Curl.php
Adds typed cURL handle (\CurlHandle|false), new getCurlHandle() helper with validation, retry policy management (setRetry() refactor), and hardened STDOUT initialization. Improves state consistency without changing core exec flow.
Multi-cURL Enhancements
src/Httpful/Curl/MultiCurl.php
Tracks cURL multi-handle as typed ?\CurlMultiHandle, updates handle caching logic, refines cookie type docs to array<string, mixed>, and improves resource management.
Request Enhancement – Major
src/Httpful/Request.php
Adds bearer token auth, retry logic with exponential backoff and max-time budgets, TLS/cert pinning, CA bundles, cookie file/jar, Alt-Svc/HSTS caching, proxy tunnel/no-proxy/resolve/connect-to helpers, and HTTP version negotiation. Replaces assert() with explicit null checks throwing NetworkErrorException. Introduces _resolveCurlHttpVersion(), _configureRetryBehavior(), and refactors redirect behavior.
Response Enhancement
src/Httpful/Response.php
Adds public transfer-info getters (getEffectiveUrl(), getPrimaryIp(), getTotalTime(), getTransferHttpVersion(), etc.), hardens header parsing (normalizes Content-Type, validates callback before invocation), and refines type annotations.
Exception & Setup
src/Httpful/Exception/ClientErrorException.php, src/Httpful/Exception/NetworkErrorException.php, src/Httpful/Exception/RequestException.php, src/Httpful/Setup.php, src/Httpful/Curl/MultiCurlPromise.php
Relaxes exception constructor parameter nullability (?\Exception, ?Callable, etc.) to match null defaults. Adds null-guard in setupGlobalMimeType() for ?string $mimeType.
Test Coverage
tests/Httpful/CurlFeatureApiTest.php, tests/Httpful/ExtraCoverageTest.php
Introduces comprehensive test suite (530 lines) for bearer auth, retry decider logic, backoff strategies, cURL option mapping, protocol versions, Alt-Svc/HSTS, and response transfer info. Activates NTLM auth assertion.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Request
    participant Curl
    participant Network
    participant Response

    Client->>Request: withBearerAuth() / withRetry() / withTls*()
    Request->>Request: Store retry policy & auth config
    Client->>Request: send()
    Request->>Request: _curlPrep()
    Request->>Curl: configureAuthAndOptions()
    Curl->>Curl: setOpt(CURLOPT_HTTPAUTH, etc.)
    Curl->>Curl: _configureRetryBehavior()
    Curl->>Network: exec() with retry loop
    Network-->>Curl: Response / timeout / connection error
    alt Retryable Error & Retries Remain
        Curl->>Curl: Apply exponential backoff
        Curl->>Network: Retry exec()
    else Max Retries or Non-Retryable
        Curl-->>Request: Final response or error
    end
    Request->>Response: Parse & populate transfer info
    Response->>Response: getEffectiveUrl() / getTotalTime() / getTransferHttpVersion()
    Response-->>Client: Completed Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops of joy for stricter types,
Bearer tokens, retry hypes,
cURL's refined with TLS care,
HTTP/2 and HTTP/3 pair,
Response transfer info blooms so bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[+]: PHP 8.0+ rework' directly and clearly summarizes the main objective of the changeset—upgrading the codebase to PHP 8.0+ with comprehensive type improvements and modernization.
Docstring Coverage ✅ Passed Docstring coverage is 84.44% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch php8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 46 minutes and 46 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request upgrades the library to PHP 8.0+, bumps PHPStan to version 2.x, and introduces a comprehensive suite of cURL-inspired request and response helpers. Key additions include support for Bearer token authentication, configurable retry logic with exponential backoff, advanced TLS/SSL settings, cookie persistence, and explicit HTTP/2 and HTTP/3 selection. The changes also include bug fixes for NTLM authentication and retry timing, alongside extensive type-hinting improvements across the codebase. I have no feedback to provide as there are no review comments to assess.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Httpful/Client.php (1)

20-20: ⚠️ Potential issue | 🟠 Major

Replace implicit nullable array params with explicit ?array.

Lines 20, 32, 63, 74, 85, 96, 108, and 119 use the deprecated pattern array $param = null. PHP 8.4 requires explicit nullable types; switch to ?array $param = null.

Example fix pattern
-    public static function get_form(string $uri, array $param = null): array
+    public static function get_form(string $uri, ?array $param = null): array

Apply the same pattern across: delete, delete_request, get, get_dom, get_form, get_json, get_request, and get_xml.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Client.php` at line 20, Update the method signatures that
currently use the deprecated pattern "array $param = null" to explicit nullable
arrays by changing each parameter type to "?array $param = null"; specifically
update signatures for delete, delete_request, get, get_dom, get_form, get_json,
get_request, and get_xml so any parameter declared as "array $params = null" (or
similar) becomes "?array $params = null" to satisfy PHP 8.4 nullable typing
requirements and avoid deprecation warnings.
🧹 Nitpick comments (1)
phpstan.neon (1)

9-9: Use explicit empty list for ignoreErrors.

Replace the bare ignoreErrors: key (currently on line 8) with ignoreErrors: [] for explicit clarity. This prevents config interpretation ambiguity and aligns with PHPStan 2.x best practices.

Proposed change
-    ignoreErrors:
+    ignoreErrors: []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@phpstan.neon` at line 9, The phpstan.neon config currently contains a bare
ignoreErrors: key; replace that key with an explicit empty list by changing
ignoreErrors: to ignoreErrors: [] so the configuration explicitly specifies no
ignore rules and avoids PHPStan 2.x ambiguity—update the phpstan.neon file's
ignoreErrors entry accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 22: Fix the inconsistent list nesting under the "Features" section by
normalizing indentation for all bullets to a consistent number of spaces per
nesting level (e.g., two spaces per indent); locate the top-level "Features"
heading and adjust the nested list items including "Client Side Certificate Auth
(SSL)" and the other multi-line feature bullets so every nested bullet uses the
same indentation style and alignment to resolve MD005.

In `@src/Httpful/Handlers/XmlMimeHandler.php`:
- Line 121: Array keys and reflected property names are being passed directly
into DOMDocument::createElement via _future_createElement which can throw on
invalid XML names; add a private sanitizer (e.g. _future_normalizeElementName)
that replaces characters not allowed in XML names with underscores and ensures
the resulting name begins with a valid start character (letter, underscore or
colon), prefixing with "node_" if necessary, and call this sanitizer whenever
you pass a user-derived name into _future_createElement (both at the array-key
path where _future_createElement is invoked and the reflection property-name
path) so createElement always receives a valid XML element name.

In `@src/Httpful/Request.php`:
- Around line 3150-3166: The retry decider is currently attached for any request
with $this->retry > 0, which can replay non-idempotent methods; update
_configureRetryBehavior() to only enable automatic retries for idempotent HTTP
methods (e.g. GET, HEAD, PUT, DELETE, OPTIONS, TRACE) by checking $this->method
(normalize to uppercase) before calling
$curl->setRetry($this->_createRetryDecider()), and call $curl->setRetry(0) for
non-idempotent methods unless an explicit opt-in is present (e.g. a new
$this->unsafeRetry flag or an idempotency token header) so POST/PATCH/etc. are
not retried by default.
- Around line 320-324: initialize() can throw a LogicException from the Curl
handle guard so _curlPrep() should catch that and rethrow a
NetworkErrorException instead; wrap the call to initialize() in _curlPrep() (or
the relevant method in Request.php) in a try/catch that catches LogicException
(and optionally Throwable/Exception) and throws a NetworkErrorException with the
original exception as the previous/inner exception so callers see a consistent
NetworkErrorException on cURL init failures (refer to initialize(), _curlPrep(),
and the handle guard in Curl::... in src/Httpful/Curl/Curl.php).

In `@src/Httpful/Response.php`:
- Around line 960-977: The code currently sets $this->content_type from
$content_type_parts[0] but only reads a charset when count($content_type_parts)
=== 2; change this to scan all parameter parts after the media type for a
"charset=" parameter: after assigning $this->content_type =
trim($content_type_parts[0]) iterate the remaining $content_type_parts (starting
at index 1), for each string part trim it, split it into key/value with
explode('=', $part, 2), and if strtolower(trim($key)) === 'charset' set
$this->charset to the trimmed value (stripping surrounding quotes) and stop;
keep using the existing symbols $content_type_parts, $this->content_type, and
$this->charset.

---

Outside diff comments:
In `@src/Httpful/Client.php`:
- Line 20: Update the method signatures that currently use the deprecated
pattern "array $param = null" to explicit nullable arrays by changing each
parameter type to "?array $param = null"; specifically update signatures for
delete, delete_request, get, get_dom, get_form, get_json, get_request, and
get_xml so any parameter declared as "array $params = null" (or similar) becomes
"?array $params = null" to satisfy PHP 8.4 nullable typing requirements and
avoid deprecation warnings.

---

Nitpick comments:
In `@phpstan.neon`:
- Line 9: The phpstan.neon config currently contains a bare ignoreErrors: key;
replace that key with an explicit empty list by changing ignoreErrors: to
ignoreErrors: [] so the configuration explicitly specifies no ignore rules and
avoids PHPStan 2.x ambiguity—update the phpstan.neon file's ignoreErrors entry
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1ebce4e-3815-42d1-8fbc-f3cac56b5784

📥 Commits

Reviewing files that changed from the base of the PR and between 836887e and ceacdfb.

📒 Files selected for processing (25)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • README.md
  • composer.json
  • phpstan.neon
  • src/Httpful/Client.php
  • src/Httpful/ClientMulti.php
  • src/Httpful/Curl/Curl.php
  • src/Httpful/Curl/MultiCurl.php
  • src/Httpful/Factory.php
  • src/Httpful/Handlers/CsvMimeHandler.php
  • src/Httpful/Handlers/DefaultMimeHandler.php
  • src/Httpful/Handlers/FormMimeHandler.php
  • src/Httpful/Handlers/JsonMimeHandler.php
  • src/Httpful/Handlers/XmlMimeHandler.php
  • src/Httpful/Headers.php
  • src/Httpful/Http.php
  • src/Httpful/Request.php
  • src/Httpful/Response.php
  • src/Httpful/ServerRequest.php
  • src/Httpful/Stream.php
  • src/Httpful/UploadedFile.php
  • src/Httpful/Uri.php
  • tests/Httpful/CurlFeatureApiTest.php
  • tests/Httpful/ExtraCoverageTest.php
💤 Files with no reviewable changes (1)
  • .github/workflows/ci.yml

Comment thread README.md Outdated
}

$el = $dom->createElement($n);
$el = $this->_future_createElement($dom, (string) $n);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

PHP DOMDocument::createElement valid element-name rules; what happens with invalid characters and namespaced class strings like "App\Model\User"?

💡 Result:

PHP DOMDocument::createElement requires the element name (localName) to follow XML Name production rules from the XML specification (w3.org/TR/xml/#NT-Name). Valid rules: - Starts with NameStartChar: letters (a-zA-Z), underscore (_), colon (:), or specific Unicode characters. - Subsequent characters: NameChar, which includes NameStartChar plus digits (0-9), hyphen (-), period (.), middle dot (·), combining characters, etc. - Cannot start with "xml" in certain cases (reserved), but DOM allows it. - No spaces, control characters, or invalid Unicode. Invalid characters trigger DOM_INVALID_CHARACTER_ERR: throws DOMException (e.g., spaces cause "Invalid Character Error"). Special cases: - NUL byte (\0): No exception, but truncates the name at NUL. - Backslash (): Valid NameChar per XML spec (Unicode category Pc or similar), so "App\Model\User" is valid. Creates element with tagName "App\Model\User" (double backslashes preserved as literal characters). Not a namespace (use createElementNS for xmlns:prefix). Results confirmed via PHP manual user notes and StackOverflow tests matching XML Name rules. For namespaced elements like "App:Model:User", use createElementNS(null, "App:Model:User") or define proper namespace/prefix.

Citations:


🏁 Script executed:

# Examine the file and look at the specific lines mentioned
wc -l src/Httpful/Handlers/XmlMimeHandler.php

Repository: voku/httpful

Length of output: 100


🏁 Script executed:

# Read the relevant sections around lines 121, 155, 197
sed -n '115,130p' src/Httpful/Handlers/XmlMimeHandler.php

Repository: voku/httpful

Length of output: 463


🏁 Script executed:

# Check line 155
sed -n '150,165p' src/Httpful/Handlers/XmlMimeHandler.php

Repository: voku/httpful

Length of output: 817


🏁 Script executed:

# Check line 197 and surrounding context
sed -n '190,205p' src/Httpful/Handlers/XmlMimeHandler.php

Repository: voku/httpful

Length of output: 655


🏁 Script executed:

# Also check the helper method _future_createElement
sed -n '171,185p' src/Httpful/Handlers/XmlMimeHandler.php

Repository: voku/httpful

Length of output: 407


Validate array keys and property names before using as XML element names.

Array keys (Line 121) and reflection property names (Line 197) can contain characters invalid for XML element names (spaces, control characters), causing DOMDocument::createElement() to throw exceptions. While class names with namespace separators (Line 155) are technically valid per the XML specification, arbitrary user-derived names require validation.

Consider normalizing keys and property names using a sanitization function that replaces invalid characters and ensures the name starts with a valid XML name character. For example:

Suggested implementation
private function _future_normalizeElementName(string $name): string
{
    // Replace invalid characters with underscores
    $normalized = \preg_replace('/[^A-Za-z0-9_:.-]+/', '_', $name) ?? '';
    
    // Ensure name starts with valid character (letter, underscore, or colon)
    if ($normalized === '' || \preg_match('/^[^A-Za-z_:]/', $normalized) === 1) {
        $normalized = 'node_' . $normalized;
    }

    return $normalized;
}

Apply this at Lines 121 and 197 where arbitrary user data is passed as element names.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Handlers/XmlMimeHandler.php` at line 121, Array keys and
reflected property names are being passed directly into
DOMDocument::createElement via _future_createElement which can throw on invalid
XML names; add a private sanitizer (e.g. _future_normalizeElementName) that
replaces characters not allowed in XML names with underscores and ensures the
resulting name begins with a valid start character (letter, underscore or
colon), prefixing with "node_" if necessary, and call this sanitizer whenever
you pass a user-derived name into _future_createElement (both at the array-key
path where _future_createElement is invoked and the reflection property-name
path) so createElement always receives a valid XML element name.

Comment thread src/Httpful/Request.php
Comment on lines 320 to +324
$this->initialize();
\assert($this->curl instanceof Curl);
$curl = $this->curl;
if ($curl === null) {
throw new NetworkErrorException('Unable to initialize cURL.');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap cURL init failures here; the null check never catches them.

initialize() either assigns a Curl instance or throws first. With the new handle guard in src/Httpful/Curl/Curl.php, a failed curl_init() now bubbles out as a LogicException, so _curlPrep() no longer consistently throws NetworkErrorException on initialization failure.

Suggested fix
-        $this->initialize();
-        $curl = $this->curl;
-        if ($curl === null) {
-            throw new NetworkErrorException('Unable to initialize cURL.');
-        }
+        try {
+            $this->initialize();
+        } catch (\LogicException $e) {
+            throw new NetworkErrorException('Unable to initialize cURL.', 0, $e);
+        }
+
+        $curl = $this->curl;
+        if ($curl === null) {
+            throw new NetworkErrorException('Unable to initialize cURL.');
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Request.php` around lines 320 - 324, initialize() can throw a
LogicException from the Curl handle guard so _curlPrep() should catch that and
rethrow a NetworkErrorException instead; wrap the call to initialize() in
_curlPrep() (or the relevant method in Request.php) in a try/catch that catches
LogicException (and optionally Throwable/Exception) and throws a
NetworkErrorException with the original exception as the previous/inner
exception so callers see a consistent NetworkErrorException on cURL init
failures (refer to initialize(), _curlPrep(), and the handle guard in Curl::...
in src/Httpful/Curl/Curl.php).

Comment thread src/Httpful/Request.php
Comment on lines +3150 to +3166
private function _configureRetryBehavior(): void
{
$curl = $this->curl;
if ($curl === null) {
throw new \LogicException('cURL is not initialized.');
}

$curl->attempts = 0;
$curl->retries = 0;

if ($this->retry <= 0) {
$curl->setRetry(0);

return;
}

$curl->setRetry($this->_createRetryDecider());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't enable automatic retries for non-idempotent methods by default.

This attaches the retry decider for any request with retry > 0, including POST and PATCH. A timeout or 5xx does not mean the first attempt was not applied, so this can replay writes and duplicate side effects. Please gate automatic retries to idempotent methods by default, or require an explicit unsafe-retry opt-in/idempotency mechanism.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Request.php` around lines 3150 - 3166, The retry decider is
currently attached for any request with $this->retry > 0, which can replay
non-idempotent methods; update _configureRetryBehavior() to only enable
automatic retries for idempotent HTTP methods (e.g. GET, HEAD, PUT, DELETE,
OPTIONS, TRACE) by checking $this->method (normalize to uppercase) before
calling $curl->setRetry($this->_createRetryDecider()), and call
$curl->setRetry(0) for non-idempotent methods unless an explicit opt-in is
present (e.g. a new $this->unsafeRetry flag or an idempotency token header) so
POST/PATCH/etc. are not retried by default.

Comment thread src/Httpful/Response.php
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 30, 2026

Coverage Status

coverage: 90.483% (+0.6%) from 89.902% — php8 into master

@voku voku requested a review from Copilot April 30, 2026 14:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the library for PHP 8.0+ while expanding the cURL-driven feature set (HTTP/2+HTTP/3 selection, retries, TLS/cookie/proxy helpers) and tightening typing/static analysis.

Changes:

  • Raise minimum PHP version to 8.0, update CI matrix and dev tooling (PHPStan 2.x, PHPUnit 9.6).
  • Add new Request/Response features around auth, retries/backoff, HTTP version selection, and transfer metadata helpers.
  • Add/adjust tests and type annotations to validate new behavior and keep static analysis clean.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Httpful/ExtraCoverageTest.php Updates NTLM regression test to assert CURLOPT_HTTPAUTH is preserved.
tests/Httpful/CurlFeatureApiTest.php Adds API-level tests for new cURL feature helpers (retry, TLS, cookies, HTTP versions, transfer info).
src/Httpful/Uri.php Tightens typing and phpdoc generics; constructor now requires string.
src/Httpful/UploadedFile.php Updates phpdoc types for PHP 8+ and iterable value types.
src/Httpful/Stream.php Adds phpstan annotations and improves metadata typing.
src/Httpful/Setup.php Tightens setupGlobalMimeType typing and null-handling.
src/Httpful/ServerRequest.php Improves phpdoc generics and constructor param typing.
src/Httpful/Response.php Adds transfer-info helper APIs; hardens header parsing and callback invocation.
src/Httpful/Request.php Adds many new request helpers (bearer, retry, TLS, cookies, proxy, HTTP versions) and refactors cURL prep.
src/Httpful/Http.php Adds HTTP/3 constant and improves return type annotations.
src/Httpful/Headers.php Improves typing, iterator key handling, and value normalization.
src/Httpful/Handlers/XmlMimeHandler.php Hardens DOM element creation and tightens return types.
src/Httpful/Handlers/JsonMimeHandler.php Tightens configuration array typing.
src/Httpful/Handlers/FormMimeHandler.php Tightens parse return type annotation.
src/Httpful/Handlers/DefaultMimeHandler.php Tightens constructor/config typing.
src/Httpful/Handlers/CsvMimeHandler.php Tightens parse return type annotation.
src/Httpful/Factory.php Tightens nullable param typing for PSR-17 factory methods.
src/Httpful/Exception/RequestException.php Adds nullable previous exception typing.
src/Httpful/Exception/NetworkErrorException.php Adds nullable argument typing for PHP 8+.
src/Httpful/Exception/ClientErrorException.php Adds nullable previous exception typing.
src/Httpful/Curl/MultiCurlPromise.php Makes callbacks nullable in then() signature.
src/Httpful/Curl/MultiCurl.php Switches to typed \CurlMultiHandle, refactors lifecycle/handle access.
src/Httpful/Curl/Curl.php Switches to typed \CurlHandle, refactors handle access and retry state.
src/Httpful/ClientMulti.php Updates param typing annotations for PHP 8+ collections.
src/Httpful/Client.php Updates param typing annotations and return typing for parsed helpers.
phpstan.neon Removes ignore rules and tightens analysis configuration.
composer.json Bumps minimum PHP to ^8.0; updates phpstan/phpunit dev requirements.
README.md Documents new features and adds example usage for new helpers.
CHANGELOG.md Adds unreleased notes for PHP 8+ rework and new features/fixes.
.github/workflows/ci.yml Drops PHP 7.4 from CI and makes PHPStan required on latest matrix entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Httpful/Request.php
return isset($this->parse_callback)
&&
\is_callable($this->parse_callback);
return $this->parse_callback !== null;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasParseCallback() previously ensured the callback was callable; now it returns true for any non-null value, even if it’s a non-callable string. This can lead to a fatal error when invoking the callback (e.g., in Response parsing). Fix by restoring a callability check (or ensuring getParseCallback() always returns a callable when non-null), e.g. require is_callable($this->parse_callback) in addition to non-null.

Suggested change
return $this->parse_callback !== null;
return $this->parse_callback !== null && \is_callable($this->parse_callback);

Copilot uses AI. Check for mistakes.
Comment thread src/Httpful/Request.php Outdated
public function withAddedHeader($name, $value): MessageInterface
{
if (!\is_string($name) || $name === '') {
if ($name === '') {
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation no longer rejects non-string header names (e.g., int/array). PSR-7 requires a string header name, and downstream normalization typically assumes a string. Restore the is_string($name) check (and keep the empty-string check) so invalid types consistently throw InvalidArgumentException instead of causing type errors later.

Suggested change
if ($name === '') {
if (!\is_string($name) || $name === '') {

Copilot uses AI. Check for mistakes.
Comment thread src/Httpful/Request.php Outdated
// fallback
if (!isset($this->template)) {
$this->template = new static(Http::GET, null, $this);
$this->template = new self(Http::GET, null, $this);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using new self(...) here breaks late-static-binding for subclasses: a subclassed Request will now end up with a base Request template instead of a template of the subclass, which can change fluent behavior/overrides. Prefer new static(...) (or otherwise preserve subclass type) while keeping PHPStan happy, e.g. via appropriate phpdoc assertions.

Suggested change
$this->template = new self(Http::GET, null, $this);
$this->template = new static(Http::GET, null, $this);

Copilot uses AI. Check for mistakes.
Comment thread src/Httpful/Response.php
Comment on lines +969 to 977
$this->content_type = $content_type_parts[0] ?? '';
if (
\count($content_type) === 2
\count($content_type_parts) === 2
&&
\strpos($content_type[1], '=') !== false
\strpos($content_type_parts[1], '=') !== false
) {
/** @noinspection PhpUnusedLocalVariableInspection */
list($nill, $this->charset) = \explode('=', $content_type[1]);
list($nill, $this->charset) = \explode('=', $content_type_parts[1]);
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Charset extraction is now limited to exactly two ;-separated parts. For common headers like text/html; charset=UTF-8; boundary=..., this will no longer populate $this->charset. Instead of checking count(...) === 2, scan all parts after index 0 for a charset= parameter and parse it when present.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +22 to +36
- Client Side Certificate Auth (SSL)
- Retry Configuration (count, delay, max time, all-errors, connection-refused)
- Advanced TLS Configuration (CA bundle / path, pinned public key, TLS version)
- Cookie Persistence (cookie file / cookie jar)
- Modern HTTP Version Helpers (HTTP/2 prior knowledge, HTTP/3, HTTP/3 only)
- Alt-Svc / HSTS Cache Helpers
- Proxy / Routing Helpers (no-proxy, proxy tunnel, resolve, connect-to)
- Request "Download"
- Request "Templates"
- Parallel Request (via curl_multi)
- Transfer Metadata Helpers
- PSR-3: Logger Interface
- PSR-7: HTTP Message Interface
- PSR-17: HTTP Factory Interface
- PSR-18: HTTP Client Interface
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several list items are misindented (extra leading space before -), which renders them as nested bullets and makes the feature list harder to read. Align indentation consistently so each feature is at the intended list level.

Suggested change
- Client Side Certificate Auth (SSL)
- Retry Configuration (count, delay, max time, all-errors, connection-refused)
- Advanced TLS Configuration (CA bundle / path, pinned public key, TLS version)
- Cookie Persistence (cookie file / cookie jar)
- Modern HTTP Version Helpers (HTTP/2 prior knowledge, HTTP/3, HTTP/3 only)
- Alt-Svc / HSTS Cache Helpers
- Proxy / Routing Helpers (no-proxy, proxy tunnel, resolve, connect-to)
- Request "Download"
- Request "Templates"
- Parallel Request (via curl_multi)
- Transfer Metadata Helpers
- PSR-3: Logger Interface
- PSR-7: HTTP Message Interface
- PSR-17: HTTP Factory Interface
- PSR-18: HTTP Client Interface
- Client Side Certificate Auth (SSL)
- Retry Configuration (count, delay, max time, all-errors, connection-refused)
- Advanced TLS Configuration (CA bundle / path, pinned public key, TLS version)
- Cookie Persistence (cookie file / cookie jar)
- Modern HTTP Version Helpers (HTTP/2 prior knowledge, HTTP/3, HTTP/3 only)
- Alt-Svc / HSTS Cache Helpers
- Proxy / Routing Helpers (no-proxy, proxy tunnel, resolve, connect-to)
- Request "Download"
- Request "Templates"
- Parallel Request (via curl_multi)
- Transfer Metadata Helpers
- PSR-3: Logger Interface
- PSR-7: HTTP Message Interface
- PSR-17: HTTP Factory Interface
- PSR-18: HTTP Client Interface

Copilot uses AI. Check for mistakes.
@voku
Copy link
Copy Markdown
Owner Author

voku commented Apr 30, 2026

@copilot apply changes based on the comments in this thread

…ack, charset, README)

Agent-Logs-Url: https://github.com/voku/httpful/sessions/9399ebc4-8b6c-4d3f-81bc-2183620fff6e

Co-authored-by: voku <264695+voku@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented Apr 30, 2026

@copilot apply changes based on the comments in this thread

Applied all five review suggestions in commit 8aa00de:

  1. hasParseCallback() — now checks is_callable() in addition to non-null, preventing fatal errors on non-callable values.
  2. withAddedHeader() header validation — restored !is_string($name) check so non-string header names throw InvalidArgumentException.
  3. new self(...)new static(...) — template construction now uses late-static-binding for correct subclass behavior.
  4. Charset extraction — replaced the count() === 2 guard with a loop over all content-type parts searching for a charset= parameter, so multi-parameter headers (e.g. text/html; charset=UTF-8; boundary=...) are handled correctly.
  5. README.md indentation — all feature list items are now consistently indented with a single leading space.

Copilot finished work on behalf of voku April 30, 2026 14:18
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
src/Httpful/Headers.php (1)

12-15: ⚡ Quick win

Align internal phpdocs with the new generic contract.

The class-level generics are now precise, but internal docs still use broad types (e.g., $data as mixed[]), which weakens static analysis and makes the unguarded inner iteration in toArray() harder to verify.

Proposed docblock tightening
-    /**
-     * `@var` mixed[] data storage with lowercase keys
+    /**
+     * `@var` array<string, array<int, string>> data storage with lowercase keys
      *
      * `@see` offsetSet()
      * `@see` offsetExists()
      * `@see` offsetUnset()
      * `@see` offsetGet()
      * `@see` count()
      * `@see` current()
      * `@see` next()
      * `@see` key()
      */
     private $data = [];

-    /**
-     * `@var` string[] case-sensitive keys
+    /**
+     * `@var` array<string, string> case-sensitive keys
      *
      * `@see` offsetSet()
      * `@see` offsetUnset()
      * `@see` key()
      */
     private $keys = [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Headers.php` around lines 12 - 15, The class-level phpdoc
generics were tightened but internal docs (e.g., the $data property and related
internal types) still use broad types like mixed[], making static analysis and
the unguarded loop inside toArray() unreliable; update the docblocks for the
Headers class to reflect the precise generic types (ArrayAccess<string,
array<int, string>> / Iterator<string, array<int, string>>), tighten the $data
property docblock to array<string, array<int, string>> (or the equivalent
precise type used elsewhere), and adjust the toArray() method/docblock to
indicate it iterates over array<string, array<int, string>> so static analyzers
can verify the inner iteration and prevent unguarded mixed usage (refer to the
Headers class, $data property, and toArray() method).
src/Httpful/Curl/MultiCurlPromise.php (1)

40-54: 💤 Low value

PHPDoc doesn't reflect nullable callable parameters.

The signature now uses ?callable but PHPDoc at Lines 46-47 still shows @param callable. Consider updating:

-    * `@param` callable $onComplete Called when a response will be available
-    * `@param` callable $onRejected Called when an error happens.
+    * `@param` callable|null $onComplete Called when a response will be available
+    * `@param` callable|null $onRejected Called when an error happens.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Curl/MultiCurlPromise.php` around lines 40 - 54, Update the
PHPDoc for the MultiCurlPromise::then method to reflect the nullable callable
types: change the two `@param` annotations for $onComplete and $onRejected from
"@param callable" to "@param callable|null" (and you can also update the textual
description to mention null is allowed) so the docblock matches the ?callable
signature in then(?callable $onComplete = null, ?callable $onRejected = null).
src/Httpful/Setup.php (1)

147-158: 💤 Low value

PHPDoc doesn't reflect the nullable parameter.

The method signature now accepts ?string $mimeType, but the PHPDoc at Line 148 still declares @param string $mimeType. Update for consistency:

    /**
-    * `@param` string $mimeType
+    * `@param` string|null $mimeType
     *
     * `@return` MimeHandlerInterface
     */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Setup.php` around lines 147 - 158, Update the PHPDoc for
Setup::setupGlobalMimeType to reflect the nullable parameter: change the `@param`
annotation from "@param string $mimeType" to a nullable type such as "@param
string|null $mimeType" (or "@param ?string $mimeType") so it matches the method
signature; verify the docblock near the call to self::initMimeHandlers() and
references to self::$mime_registrar remain unchanged.
src/Httpful/Request.php (1)

1656-1659: ⚡ Quick win

Redundant is_callable check.

Static analysis correctly identifies that if $this->parse_callback !== null, then is_callable($this->parse_callback) is always true since the property is typed callable|null. The second condition can be removed:

 public function hasParseCallback(): bool
 {
-    return $this->parse_callback !== null && \is_callable($this->parse_callback);
+    return $this->parse_callback !== null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Request.php` around lines 1656 - 1659, The hasParseCallback()
method includes a redundant is_callable() check because the parse_callback
property is typed callable|null; replace the body to simply return whether
parse_callback is non-null (e.g., return $this->parse_callback !== null;) so
static analyzers stop flagging it; keep the method name hasParseCallback and the
property parse_callback unchanged.
src/Httpful/Client.php (1)

59-60: Add ?string type hint to $mime parameter in Client::get() and Client::get_request().

The PHPDoc comments document string|null for the $mime parameter at lines 60 and 104-105, but the function signatures at lines 63 and 108 leave it untyped. Adding ?string type hints would make the API consistent with its documentation and the Request::get() implementation, which already uses ?string $mime = null. The default value Mime::PLAIN is a string constant, making it fully compatible with the type hint.

Proposed diff
-    public static function get(string $uri, ?array $params = null, $mime = Mime::PLAIN): Response
+    public static function get(string $uri, ?array $params = null, ?string $mime = Mime::PLAIN): Response
@@
-    public static function get_request(string $uri, ?array $param = null, $mime = Mime::PLAIN): Request
+    public static function get_request(string $uri, ?array $param = null, ?string $mime = Mime::PLAIN): Request
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Httpful/Client.php` around lines 59 - 60, The Client::get() and
Client::get_request() signatures lack the nullable string type hint for the
$mime parameter despite PHPDoc and Request::get() documenting ?string $mime =
null; update both method declarations (Client::get and Client::get_request) to
declare ?string $mime = Mime::PLAIN (or ?string $mime = null if preferred) so
the runtime signature matches the docs and Request::get(); ensure no other
callers break and run tests/type checks after changing the signatures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Httpful/Client.php`:
- Around line 59-60: The Client::get() and Client::get_request() signatures lack
the nullable string type hint for the $mime parameter despite PHPDoc and
Request::get() documenting ?string $mime = null; update both method declarations
(Client::get and Client::get_request) to declare ?string $mime = Mime::PLAIN (or
?string $mime = null if preferred) so the runtime signature matches the docs and
Request::get(); ensure no other callers break and run tests/type checks after
changing the signatures.

In `@src/Httpful/Curl/MultiCurlPromise.php`:
- Around line 40-54: Update the PHPDoc for the MultiCurlPromise::then method to
reflect the nullable callable types: change the two `@param` annotations for
$onComplete and $onRejected from "@param callable" to "@param callable|null"
(and you can also update the textual description to mention null is allowed) so
the docblock matches the ?callable signature in then(?callable $onComplete =
null, ?callable $onRejected = null).

In `@src/Httpful/Headers.php`:
- Around line 12-15: The class-level phpdoc generics were tightened but internal
docs (e.g., the $data property and related internal types) still use broad types
like mixed[], making static analysis and the unguarded loop inside toArray()
unreliable; update the docblocks for the Headers class to reflect the precise
generic types (ArrayAccess<string, array<int, string>> / Iterator<string,
array<int, string>>), tighten the $data property docblock to array<string,
array<int, string>> (or the equivalent precise type used elsewhere), and adjust
the toArray() method/docblock to indicate it iterates over array<string,
array<int, string>> so static analyzers can verify the inner iteration and
prevent unguarded mixed usage (refer to the Headers class, $data property, and
toArray() method).

In `@src/Httpful/Request.php`:
- Around line 1656-1659: The hasParseCallback() method includes a redundant
is_callable() check because the parse_callback property is typed callable|null;
replace the body to simply return whether parse_callback is non-null (e.g.,
return $this->parse_callback !== null;) so static analyzers stop flagging it;
keep the method name hasParseCallback and the property parse_callback unchanged.

In `@src/Httpful/Setup.php`:
- Around line 147-158: Update the PHPDoc for Setup::setupGlobalMimeType to
reflect the nullable parameter: change the `@param` annotation from "@param string
$mimeType" to a nullable type such as "@param string|null $mimeType" (or "@param
?string $mimeType") so it matches the method signature; verify the docblock near
the call to self::initMimeHandlers() and references to self::$mime_registrar
remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92678ccd-da95-488f-98ef-8e633042f51c

📥 Commits

Reviewing files that changed from the base of the PR and between ceacdfb and 8aa00de.

📒 Files selected for processing (16)
  • README.md
  • src/Httpful/Client.php
  • src/Httpful/ClientMulti.php
  • src/Httpful/Curl/MultiCurl.php
  • src/Httpful/Curl/MultiCurlPromise.php
  • src/Httpful/Exception/ClientErrorException.php
  • src/Httpful/Exception/NetworkErrorException.php
  • src/Httpful/Exception/RequestException.php
  • src/Httpful/Factory.php
  • src/Httpful/Handlers/XmlMimeHandler.php
  • src/Httpful/Headers.php
  • src/Httpful/Request.php
  • src/Httpful/Response.php
  • src/Httpful/ServerRequest.php
  • src/Httpful/Setup.php
  • src/Httpful/Uri.php
✅ Files skipped from review due to trivial changes (1)
  • src/Httpful/Exception/ClientErrorException.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/Httpful/Factory.php
  • src/Httpful/Response.php
  • src/Httpful/ClientMulti.php
  • src/Httpful/ServerRequest.php

Agent-Logs-Url: https://github.com/voku/httpful/sessions/1a96e2f8-8c99-482b-833a-e02e774b09e5

Co-authored-by: voku <264695+voku@users.noreply.github.com>
Copilot AI and others added 2 commits April 30, 2026 20:18
Agent-Logs-Url: https://github.com/voku/httpful/sessions/e1af6f48-c4a7-4c8e-baa9-fe8e4fa18e3c

Co-authored-by: voku <264695+voku@users.noreply.github.com>
Agent-Logs-Url: https://github.com/voku/httpful/sessions/e1af6f48-c4a7-4c8e-baa9-fe8e4fa18e3c

Co-authored-by: voku <264695+voku@users.noreply.github.com>
Copilot finished work on behalf of voku April 30, 2026 20:20
@voku voku merged commit a6fd99f into master Apr 30, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants