Library / SDK
amphp/http-client avatar
amphp/http-client

amphp/http-client: an async HTTP client for PHP that does not use cURL

An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.

733 stars69 forksPHPMIT

At a glance

What is it?
This is the HTTP client from the AMPHP family of event-driven PHP libraries, built on Revolt, and it implements HTTP over TCP sockets itself so that it has no dependency on ext-curl. The interesting part is the design consequence of that choice: requests are concurrent by default, connections are pooled, and bodies stream, which makes it a different shape of client from the one PHP developers usually reach for.
Who is it for?
Adopt amphp/http-client if you are writing concurrent PHP that has to talk to many remote services at once, since the request model is async by default and the connection pool is the point. Do not adopt it if you need a synchronous one-shot call in a legacy script, because the fiber-based runtime expects an event loop.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 116 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The choice that defines the library: no ext-curl dependency

AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind, and this package is the asynchronous HTTP client within it, based on Revolt.

The sentence that explains the whole design is short. The library manually implements HTTP over TCP sockets, as such it has no dependency on ext/curl.

That is unusual for a PHP HTTP client. The traditional choice is to wrap libcurl through the curl extension, and requests then spend most of their time inside a C API rather than in PHP code, which is harder to integrate with a fiber-based event loop. Implementing HTTP directly over sockets lets the client participate in the fiber-based event loop, which is what makes it possible to have many requests in flight at once in a single process.

The API goal is stated as simplifying standards-compliant HTTP resource traversal and RESTful web service consumption without obscuring the underlying protocol. That is a promise about not hiding how HTTP actually works, and it is consistent with a library that implements the protocol itself rather than delegating.

The feature list is long and specific about the transport. It supports HTTP/1 and HTTP/2. It requests concurrently by default. It pools persistent connections, with keep-alive at HTTP/1.1 and multiplexing at HTTP/2. It transparently follows redirects, decodes compressed entity bodies for gzip and deflate, exposes headers and message data, streams entity bodies for memory management with large transfers, supports all standard and custom HTTP method verbs, simplifies HTTP form submissions, implements secure-by-default TLS for https URLs, supports cookies and sessions, and works behind HTTP proxies.

The homepage is amphp.org/http-client, and the licence is MIT.

Building a client, and the two kinds of interceptor

The main interaction point with the library is the HttpClient class, and HttpClient instances can be built using HttpClientBuilder without knowing about the existing implementations. That last clause matters, because it means the client implementation is a detail you do not have to know about.

The builder is also where the extension mechanism lives. HttpClientBuilder allows you to register two kinds of interceptors, which allows customizing the HttpClient behavior in a composable fashion. Interceptors are the standard way to add cross-cutting behaviour to an HTTP client, and being able to compose them rather than subclass is what keeps a client with a retry policy, a logging layer and a metrics layer from becoming a tangle.

The simplest usage looks like this:

php
use Amp\Http\Client\HttpClientBuilder;

$client = HttpClientBuilder::buildDefault();

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

var_dump($response->getStatus());
var_dump($response->getHeaders());
var_dump($response->getBody()->buffer());

In its simplest form, the HTTP client takes a request with a URL as string and interprets that as a GET request to that resource without any custom headers, and standard headers like Accept, Connection or Host will automatically be added if not present.

That last detail is the kind of small convenience that stops being a convenience when it is missing. A client that sends a request without a Host header will be rejected by many servers, and a client that omits Accept will get a different response than you expected.

The three calls on the response are the whole read path: the status, the headers, and the body. The body call is the interesting one, because getBody() returns a Payload, and the buffer() call on it reads the whole thing into memory. The streaming alternative is documented separately, and for a large download that is the difference between a bounded and an unbounded memory footprint.

Request objects are mutable, which is a departure from the PSR-7 habit

The Request class can be used to specify further specifics of the request such as setting headers or changing the request method, and the documentation is upfront about one thing that will surprise anyone coming from PSR-7 or from the earlier artax library.

Request objects are mutable, instead of immutable as in amphp/artax / PSR-7. Cloning Request objects will result in a deep clone, but doing so is usually only required if requests are retried or cloned for sub-requests.

So the request is a builder you configure in place, and you clone it only when you need two divergent copies of the same starting point. The reason cloning matters is stated: a retry or a redirect means the same request may be sent more than once, so you need independent copies of it.

The setters follow a consistent pattern. setUri changes the request URI and the constructor requires an absolute one. setMethod changes the request method and the constructor accepts an optional method defaulting to GET. The body is set with setBody, and the accepted types are string, null and HttpContent, with string and null automatically converted to an instance of HttpContent.

That last conversion has a reason, and it is documented in a note. HttpContent is basically a factory for request bodies. We cannot simply accept streams here, because a request body might have to be sent again on a redirect or retry.

That is a real constraint rather than an implementation detail. A PHP stream is inherently one-shot, and an HTTP client that supports redirects and retries needs a body it can send twice. HttpContent gives it that, at the cost of a slightly more awkward API.

The header API is the other place with careful semantics. setHeader removes any previous values for that field, while addHeader adds an additional header line without removing existing lines, and setHeaders sets multiple at once with the array keys being the field names and the values being the header values, where the values can also be arrays of strings to set multiple header lines. The getters mirror that: hasHeader checks whether at least one line exists, getHeader returns the first line or null, and getHeaderArray returns all of them as an array, empty if there are none.

Responses arrive before the body, and the body streams

HttpClient::request() returns a Response as soon as the response headers are successfully received. That single sentence is the API's most consequential design decision, and it is worth thinking about what it buys.

Because the promise resolves at the headers, you know the status code and the content length before the body has arrived. That means you can abort a download the moment you see a 404, and you can make a routing decision based on the status without waiting for a body you are going to discard. In a client that fans out to many services, that is a meaningful amount of wasted transfer avoided.

The same line explains why streaming exists as a separate feature. The body is not read into a string when the response arrives, so a large response does not have to be buffered. The feature list calls this streaming entity bodies for memory management with large transfers, and the examples directory has a streaming/ folder next to a basic/ one.

Response objects are mutable as well, instead of immutable as in Artax v3 / PSR-7, matching the mutability of the request side.

The accessors are symmetric with the request headers. hasHeader returns whether a given header is present, getHeader returns the first header with that name or null, getHeaderArray returns all of them and possibly an empty array, and getHeaders returns all headers as an associative array. The format of that array is documented explicitly, with the field name as the key and an array of values under it:

php
[
    "header-1" => [
        "value-1",
        "value-2",
    ],
    "header-2" => [
        "value-1",
    ],
]

Always an array of values, never a bare string, which is the correct choice for HTTP because a header can legitimately appear more than once. The status has its own accessors, getStatus() returning an integer and getReason() returning the optional and possibly empty reason associated with the status, and getProtocolVersion() exposing the HTTP protocol version of the response.

Concurrency, pooling and the optional nghttp2 speedup

Three of the design choices interact, and they are the reason to pick this client over a synchronous one.

Requests concurrently by default is the first. The examples directory has a concurrency/ folder with a file named 1-concurrent-fetch.php, so the documented way to see the behaviour is to fetch several things at once. The word default matters, because a client that only supports concurrent use is awkward for the case where you genuinely want to wait for one response before deciding what to request next.

Pools persistent connections is the second, with keep-alive at HTTP/1.1 and multiplexing at HTTP/2. This is the difference between opening a TCP connection, doing a TLS handshake and then a request for every call, and reusing what is already there. The examples/pooling/ folder has a file named 1-connection-count.php, which is a measurement of exactly that. On a workload that makes many requests to the same host, the connection count is the number that tells you whether the pool is working.

The third is the recommendation about nghttp2. Additionally, you might want to install the nghttp2 library to take advantage of FFI to speed up and reduce the memory usage.

So the HTTP/2 implementation has two paths. One uses the nghttp2 library loaded through PHP's FFI, which is faster and uses less memory. The other is presumably a pure PHP implementation, which is what you get if you do not install the extension. That is a reasonable trade for a library that otherwise has no compile-time dependencies: the common case works out of the box, and the people who need the performance have a documented upgrade path.

It is also worth connecting this back to the no-curl decision. Because the client implements HTTP over TCP sockets itself, the protocol handling is the library's own code, and the question of which HTTP/2 implementation to use is a question the library has to answer for you. The FFI route to nghttp2 is that answer.

The package layout and where the supported versions are

The repository layout is a conventional PHP library with a lot of example code, and the examples are organised by the feature they demonstrate.

There is a basic/ folder with separate files for the GET request, the headers, the form submission and the gzip decoding, so each documented feature has a runnable file attached to it. Then concurrency/, pagination/, pooling/, proxy/ and streaming/ folders, plus a .helper/ directory. The pagination and proxy folders are worth noticing, because they are features that many HTTP clients treat as an afterthought and that this one lists in its feature list, with transparently follows redirects and works behind HTTP proxies as the summary lines.

The src/ directory holds the implementation, with test/ and phpunit.xml.dist for the test suite and psalm.xml for static analysis, plus a .php-cs-fixer.dist.php for style. There is a composer-require-check.json, which is the configuration for the tool that verifies that the dependencies you declare are the ones you actually use, and a CHANGELOG.md.

The versioning is the part to check before you depend on it. The default branch is 5.x, and the release list shows v5.3.6 on 2026-05-16, v5.3.5 on 2026-05-14 and 4.6.6 on the same day. The 4.6.6 release alongside 5.3.5 tells you the 4.x line is still receiving maintenance releases, which is the usual pattern for a library where users upgrade on their own schedule and the maintainers support the older line for a while.

The last push to the repository was on 2026-06-29, and the repository is not archived.

The one thing to know before you write code against it is that the package is a dependency of the wider AMPHP stack, and the runtime is Revolt, so the client expects to be hosted inside an event loop. That is not a limitation so much as a requirement, and it is the same requirement every other library in the family has.

Editorial conclusion

Adopt amphp/http-client if you are writing concurrent PHP that has to talk to many remote services at once, since the request model is async by default and the connection pool is the point. Do not adopt it if you need a synchronous one-shot call in a legacy script, because the fiber-based runtime expects an event loop. Before you install, check that your PHP setup can host Revolt, and decide whether you want the nghttp2 FFI extension, since the documentation recommends it to speed up the client and reduce memory usage.

Frequently asked questions

Does amphp/http-client need the cURL extension?

No. The library manually implements HTTP over TCP sockets, so it has no dependency on ext/curl, which is what allows it to run inside the fiber-based event loop rather than blocking on a C API designed for one request at a time.

How do I install amphp/http-client?

Add it as a Composer dependency with composer require amphp/http-client. You might also want to install the nghttp2 library to take advantage of FFI to speed up the client and reduce memory usage.

When does the response promise resolve in amphp/http-client?

HttpClient::request() returns a Response as soon as the response headers are successfully received, before the body has arrived. The body is then read from the Response, and can be streamed to keep memory bounded on large transfers.

How do I customize the behaviour of the amphp HTTP client?

HttpClientBuilder allows you to register two kinds of interceptors, which lets you customize the HttpClient behavior in a composable fashion. The client itself is built with HttpClientBuilder::buildDefault() so you do not need to know about the existing implementations.

Official sources

  1. amphp/http-client on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/amphp-http-client.svg)](https://hysenlabs.com/projects/amphp-http-client)