Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Http Client Laravel Package

amphp/http-client

A fully asynchronous HTTP/1 and HTTP/2 client for PHP built on Revolt, with no cURL dependency. It supports concurrent requests, connection pooling, redirects, compression, streaming bodies, cookies, and proxies through a composable interceptor API.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're buying first. amphp/http-client is not a PSR-18 / PSR-7 client. It is an Amp v3 (Revolt + fibers) library with its own mutable Request/Response objects. If your framework is wired around PSR-18 interfaces, you'll be writing an adapter or keeping a second client around for those code paths.

Requirements: PHP 8.1+ (fibers), revolt/event-loop, and no ext/curl — the library speaks HTTP over raw TCP sockets itself. Optionally install the nghttp2 library to enable the FFI-based HTTP/2 implementation (lower memory, faster HPACK).

composer require amphp/http-client

Minimal first request:

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

$client = HttpClientBuilder::buildDefault();

$response = $client->request(new Request('https://httpbin.org/get'));

var_dump($response->getStatus());
var_dump($response->getHeaders());          // ['name' => ['value', ...]]
var_dump($response->getBody()->buffer());   // whole body as string

That's the entire happy path. No event loop to start, no Amp\run() — the loop runs implicitly around your fiber.

Where to look first, in this order:

  1. examples/ in the repo — basic/, concurrency/, pooling/, streaming/. These are the real documentation; the README stops at the surface.
  2. src/HttpClientBuilder.php — this class is the actual inventory of what the client can do (intercept(), interceptNetwork(), listen(), followRedirects(), retry(), usingPool()). Reading its method list takes two minutes and beats hunting through blog posts.
  3. changelog/ — the per-release notes flag behavioural fixes (timeouts, HTTP/2 buffering, connection reuse) that explain weird production behaviour.
  4. Interceptor/ directory — a set of small, composable classes that double as worked examples of the interceptor contract.

Your first real use case is almost always "fan out to N JSON endpoints and aggregate." Start there rather than with a single request, because concurrency is the whole reason to pick this package over Guzzle:

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\await;

$client = HttpClientBuilder::buildDefault();

$futures = [];
foreach ($urls as $url) {
    $futures[$url] = async(fn () => $client->request(new Request($url))->getBody()->buffer());
}

$bodies = await($futures); // ['url' => 'body', ...]

Implementation Patterns

Build one client, inject it, reuse it

Connection pooling lives inside the HttpClient instance. Building a client per call silently destroys keep-alive and HTTP/2 multiplexing. Treat it as a singleton in your container:

// bootstrap / service provider
$container->singleton(HttpClient::class, fn () => HttpClientBuilder::buildDefault());

Configuration belongs on the builder, not on individual requests — that's what makes interceptors reusable across every call site.

Interceptors for cross-cutting concerns

Almost everything you'd otherwise copy-paste belongs in an interceptor. Add auth headers, tenant IDs, correlation IDs, timing metrics:

$client = (new HttpClientBuilder)
    ->intercept(new SetRequestHeader('authorization', "Bearer {$token}"))
    ->intercept(new SetRequestHeader('x-request-id', $requestId))
    ->build();

For anything conditional, use ModifyRequest / ConditionalInterceptor rather than writing a class from scratch:

->intercept(new ConditionalInterceptor(
    new IfOrigin('api.internal.example'),
    new SetRequestHeader('x-internal-auth', $secret),
))

Application vs network interceptors — the rule of thumb:

  • Application interceptor (default): runs around the whole request/response lifecycle, can be slow, can short-circuit (cache hits, retries). This is where 95% of your logic goes.
  • Network interceptor: only runs once a connection exists; useful for recording the resolved IP, TLS details, connection ID. It must be fast — the client's inactivity timeout is ticking while it runs.

Redirects and auth: set headers via interceptors, not the request

This is subtle and it bites people. With HttpClientBuilder, FollowRedirects is the outermost interceptor, and cross-origin redirects are re-issued with no application headers. Headers set on the Request directly are lost across a redirect hop; headers set by an interceptor are re-applied on each hop. So:

// survives redirects
$client = (new HttpClientBuilder)->intercept(new SetRequestHeader('x-api-key', $key))->build();

// does NOT survive a cross-origin redirect
$request = new Request($url);
$request->setHeader('x-api-key', $key);

Tune or disable redirects with ->followRedirects(0) or ->followRedirects(5).

Streaming large downloads instead of buffering

getBody()->buffer() pulls the entire response into memory. For files, pipe:

use function Amp\ByteStream\pipe;

$response = $client->request(new Request($url));
pipe($response->getBody(), $fileStream); // WritableStream from amphp/file or a socket

For incremental processing (NDJSON, SSE-ish feeds, chunked APIs):

$body = $response->getBody();
while (null !== $chunk = $body->read()) {
    $parser->feed($chunk);
}

Upload symmetric: Request::setBody() accepts string|null|HttpContent. Use FormBody for form posts and an HttpContent implementation for file/stream uploads — not raw streams, because the body may need to be replayed on a retry or a 307/308 redirect.

Retries and timeouts as configuration

$client = (new HttpClientBuilder)
    ->retry(3)                    // RetryRequests interceptor
    ->followRedirects(5)
    ->intercept(new SetRequestTimeout(10_000)) // ms; check exact signature for your version
    ->build();

Retries only help with idempotent verbs — the default retry interceptor will not save you from a double-charged POST. Be explicit about which requests are safe to retry, often by wrapping ->retry() only in the client instance used for GET-heavy read paths.

Cookies and proxies are separate packages

The core library is deliberately minimal:

// composer require amphp/http-client-cookies
$client = (new HttpClientBuilder)
    ->intercept(new CookieInterceptor(new LocalCookieJar()))
    ->build();

Proxies come from amphp/http-tunnel. Caching from amphp/http-client-cache (PrivateCache, a fully compliant interceptor). Check each package's README for exact class names — they've been renamed across major versions.

Error handling: status codes are not exceptions

request() throws only for transport/protocol failures (HttpException, SocketException, TimeoutException, TlsException). A 500 is a perfectly successful request as far as the client is concerned:

$response = $client->request($request);

if ($response->getStatus() >= 400) {
    throw new ApiException($response->getStatus(), $response->getBody()->buffer());
}

If you want fail-fast semantics globally, encode it as an interceptor that throws when getStatus() is in your error range — that's the idiomatic Amp way, and it keeps call sites clean.

Cancellation

Pass a Cancellation to bail out early, which is far cleaner than timeouts for user-driven requests:

use Amp\TimeoutCancellation;

$response = $client->request($request, new TimeoutCancellation(5));

Testing

Extend Amp\PHPUnit\AsyncTestCase (from amphp/phpunit-util) so tests run inside the loop. Since HttpClient is a tiny interface, fake it directly:

final class StubClient implements HttpClient
{
    public function request(Request $request, ?Cancellation $cancellation = null): Response
    {
        return new Response('1.1', 200, 'OK', [], $this->body);
    }
}

Test interceptors in isolation — construct one, hand it a Request, delegate to a stub DelegateHttpClient, assert on what comes back. That's where your actual logic lives.

Observability: HAR logging

use Amp\Http\Client\EventListener\LogHttpArchive;

$client = (new HttpClientBuilder)
    ->listen(new LogHttpArchive('/tmp/http-client.har'))
    ->build();

Produces a HAR file with per-phase timings (DNS, connect, TLS, TTFB) that loads straight into Chrome DevTools or a HAR viewer. Excellent for diagnosing "why is this endpoint slow" when the answer is TLS handshake vs. server latency.

Gotchas and Tips

You must consume or close the response body. The connection cannot return to the pool while a body is still open. If you only care about status or headers, call $response->getBody()->close() — otherwise you'll leak connections until the pool is exhausted and requests start queuing mysteriously.

buffer() and read() are consuming, not peeking. buffer() returns what's left, not the whole body. Calling read() then buffer() gives you only the remainder. There's no rewind. Decide up front whether a response is buffered or streamed.

Request and Response are mutable. This is a deliberate divergence from PSR-7. Interceptors mutate the request in place as it flows through the chain. Consequences:

  • Don't reuse a single Request object across concurrent sends — each send needs its own instance, or clone (cloning is a deep clone, so it's safe but not free).
  • $response->getRequest() is the final request after normalization and redirects; getOriginalRequest() is what you passed in. Log the right one.

Redirect semantics are narrower than browsers.

  • Only GET redirects are followed automatically. A 307/308 on a POST is returned as-is — you must handle it or set the method yourself.
  • Response bodies of intermediate redirect hops are discarded. If you need them, set followRedirects(0) and implement the loop.
  • Chain inspection via getPreviousResponse() gives you status/headers only, not bodies.

Non-2xx is silent. Repeating this because it's the #1 first-week bug: a 404 returns a Response, not an exception. Also, getHeader() returns the first value only — use getHeaderArray() for multi-value headers like set-cookie or link.

Version drift is severe. The README shows the v5 API (mutable Request, Amp\ByteStream\Payload, Future). Most material you'll find online — including many Stack Overflow answers — is for v3/v4 with immutable Request, Promise, and yield. If a snippet uses yield or ->onResolve(), it's the wrong generation. Pin the major version in composer.json and read the changelog before upgrading.

Blocking code inside a fiber stalls the event loop. file_get_contents(), sleep(), mysqli_query(), heavy CPU work — all of these block every concurrent request in that process. Use amphp/file for disk, Amp\delay() instead of sleep(), and push CPU-bound work to a worker.

Network interceptors can trigger timeouts. They run after the connection is established and the inactivity timer is already armed. A network interceptor doing a database lookup is a production incident waiting to happen. If you need slow logic, put it in an application interceptor.

Concurrency is unbounded by default in your own async() loops. The client's connection pool limits connections (per-authority by default), but if you async() 500 URLs you'll queue 500 fibers and 500 Request objects. For large fan-outs use Amp\Pipeline\concurrentMap($items, $mapper, $concurrency) with an explicit ceiling, and remember that Amp\Future\await() throws the first error while the rest keep running — use awaitAll() when you want per-key errors.

HTTP/2 multiplexing changes your latency profile. Over HTTP/1.1, N requests means N connections from the pool. Over HTTP/2 they share one connection and are subject to flow control — a slow consumer on one stream can throttle others. Recent releases specifically fixed window-size and inactivity-timeout issues with slow consumers, so keep this package reasonably current if you stream over HTTP/2.

buffer() on a multi-gigabyte download is an OOM. There's no warning; PHP just dies. Route any download whose size you don't control through pipe().

Disable decompression carefully. The default builder decompresses gzip/deflate transparently. If you're proxying bodies byte-for-byte (e.g. re-serving an upstream response, or verifying a signature over the wire bytes), you need the raw, encoded body — check the builder for the skip-compression option rather than trying to re-encode.

HAR files contain secrets. LogHttpArchive records full URLs, headers (including authorization, cookie), and timings. Never write it to a shared path in production without scrubbing, and never attach one to a public issue.

Extension points worth knowing: EventListener (the listen() builder method — per-phase hooks for metrics/tracing; better than interceptors for timing because it sees connection-level events), the Connection\ConnectionFactory (DNS resolver, TLS options, inactivity timeout — configure when you need custom CA bundles or a non-default resolver), and HttpClientBuilder::usingPool() when you want a shared pool across multiple client configurations.

Package health note: the code is actively maintained and the changelog is unusually honest about real-world edge cases, but the ecosystem is small and tightly coupled — you're buying into Amp v3's fiber/Revolt model wholesale. The cost isn't the library, it's the surrounding dependency graph (amphp/socket, amphp/byte-stream, amphp/file, revolt/event-loop) and the fact that most PHP tooling and third-party SDKs assume PSR-18. Budget time for that boundary, and keep this client confined to the services where concurrency actually pays for itself.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus