ADR-0018: One cURL factory creation per transfer lease
- Status
-
Accepted
- Date
-
2026-10-10
- Relation
-
Supplements ADR-0003, ADR-0007 and ADR-0010.
Context
A new native connection must not bypass the terminal policy check. An SDK may request an internal retry while finishing a failed transfer, and body preparation callbacks can re-enter handle creation. Private SDK retry counters are not a stable boundary for these attempts.
The transfer lease in ADR-0010: Allow address memoization; isolate authorization and connection pooling therefore needs its own single-use boundary immediately before native handle creation. This decision details the controlled transport in ADR-0003: Two middleware boundaries and a controlled terminal transport and ADR-0007: Controlled cURL only; no proxies or PHP streams in v1; it does not restrict every independent use of cURL in the PHP process.
Decision
Single implements the public
Curl and decorates Curl.
It consumes its attempt before the first delegation. Every second
create request on the same instance fails with
transport_ before the delegate creates another handle.
This includes failed and re-entrant body preparation. release
delegates cleanup without resetting the consumed instance.
Transfer constructs a new single-use factory and its own
Curl for each attempt. Connection and DNS state are
not carried between independent leases. Fresh connections, forbidden reuse
and disabled native DNS caching supplement connection-bound pinning.
Policy validity is checked after DNS and body preparation and before
network progress starts.
Runtime validates actual supported Guzzle 7 and 8 graphs
and the public factory and handler contracts before the guarded transfer.
The single-use boundary neither reads nor changes private SDK retry
counters. A genuine caller retry needs a new policy check and lease;
policy failures do not authorize an automatic retry.
Middleware ordering has a separate, deliberate dependency on the private
Handler inventory. Runtime probes
the installed shape with a known named entry;
Guarded inspects it to verify ordering and reject
later changes. An incompatible inventory fails closed before protected
transport. This qualified coupling remains even though the factory fence
uses a public interface.
Alternatives considered
- Set a private SDK retry counter: depends on internal SDK details.
- Middleware before the first send only: does not bound hidden handle creation within the same terminal attempt.
- Reset the factory after release or failed preparation: reopens a consumed lease for another native attempt.
- Share a pool or lease between attempts: carries an earlier decision into a new connection attempt.
Consequences
A lease can call the delegate's handle creation at most once. This bounds factory attempts; it does not promise a particular number of packets or of pinned addresses tried by cURL within one handle. Callback and SDK retry failures remain controlled errors. Cleanup and cancellation still belong to the lease; independent transfers own separate resources and do not reuse connections.
Compatible SDK patches within the supported semantic ranges do not require an extension release merely because their version changes. A changed public interface or unsupported major graph is rejected before the guarded transfer and requires renewed qualification. A private middleware inventory change also requires requalification; public factory compatibility alone does not establish compatibility of the entire SDK integration.
Sources and evidence
The implementation is in
Classes/,
Classes/ and
Classes/. The separately
qualified private inventory use is in
Classes/.
Tests/ maintains
create/release, failed preparation and re-entry controls. Its genuine
public Curl comparison admits a second handle
without the fence and rejects it with the fence. That test creates native
handles but performs no handler tick or network I/O.
Tests/
maintains lease option and cleanup controls; separate contact and retry
probes are in Tests/.
Tests/ maintains
incompatible public API and private inventory shape controls.
Previously executed Native and contact evidence is recorded under
Verification report. Today's source inspection for this ADR is
neither a new test run nor a new transport measurement.
Reconsideration trigger
Changes to factory or handler contracts or the private middleware inventory, alternative retry paths, connection pooling or new transport interfaces require renewed single-use, lifecycle and actual-contact qualification. Composer dependency resolution alone does not establish that compatibility.