workerman/http-client
An asynchronous, non-blocking HTTP client for Workerman. It supports GET/POST requests, file uploads and downloads, redirects, cookies, proxies, timeouts, keep-alive, and concurrent callback-based requests for high-performance PHP applications.
Pull in the package alongside Workerman itself — the client is a thin layer over Workerman\Connection\AsyncTcpConnection, so there is no separate event loop to install:
composer require workerman/http-client workerman/workerman
The single most important thing to internalise first: this client is not a drop-in replacement for Laravel's Http facade. It has no ->get() that returns a response. Every call is request(url, callbacks) and the callbacks only fire while a Workerman event loop is running. A plain php artisan tinker script that calls $client->get(...) will appear to do nothing at all.
Minimal working script — note the dummy Worker used purely to start the loop:
use Workerman\Worker;
use Workerman\Http\Client;
$worker = new Worker();
$worker->onWorkerStart = function () {
$client = new Client(['timeout' => 5]);
$client->get('https://httpbin.org/get', function ($response) {
echo $response->getStatusCode(), "\n";
echo $response->getBody(), "\n";
}, function ($exception) {
echo "ERR: ", $exception->getMessage(), "\n";
});
};
Worker::runAll();
Where to look first in the repo: src/Client.php for the option keys and the get/post/request signatures, and src/Response.php for the small surface you actually get back (getStatusCode(), getHeaders(), getBody()).
First real use case: fan-out. Fetch 50 URLs and total the wall-clock time — it should be roughly the slowest single request, not the sum. If that demo does not impress you, the package probably is not the right tool for your problem.
Reuse one Client per process. The client owns the connection pool and keep-alive sockets. Construct it once in onWorkerStart (or as a Laravel container singleton) and hand it around; new Client() inside a request handler defeats pooling and leaks sockets under load.
Fan-out with a completion counter. There is no Promise::all, so the idiomatic pattern is a shared counter plus a "done" branch:
$urls = [...];
$results = [];
$pending = count($urls);
foreach ($urls as $url) {
$client->get($url, function ($response) use (&$results, &$pending, $url) {
$results[$url] = $response->getBody();
if (--$pending === 0) { /* all done */ }
}, function ($e) use (&$results, &$pending, $url) {
$results[$url] = null;
if (--$pending === 0) { /* all done */ }
});
}
Decrement in both callbacks or you will hang forever on the first failure. For cleaner code, wrap each request in a small deferred object that resolves/rejects, or use Workerman\Coroutine\Channel if you also pull in workerman/coroutine.
Generic requests and JSON bodies. post() with an array sends application/x-www-form-urlencoded. For a JSON API you must encode and set the header yourself:
$client->request('POST', $url, [
'headers' => ['Content-Type' => 'application/json'],
'data' => json_encode($payload),
], $success, $error);
URL-encode query strings yourself ($url . '?' . http_build_query($q)) — there is no query option.
Per-call option overrides. Options passed to request() (timeout, headers, max_redirects, progress) override the client defaults for that call only. Keep the client-level defaults conservative and widen timeouts on the specific slow endpoint rather than globally.
Streaming large downloads. Use the progress callback to write chunks to disk and avoid holding the whole body in memory:
$fh = fopen($path, 'wb');
$client->get($url, function ($response) use ($fh) { fclose($fh); },
function ($e) use ($fh) { fclose($fh); },
);
// with 'progress' => function ($buffer, $response) use ($fh) { fwrite($fh, $buffer); }
Laravel integration. The realistic shape is an Artisan command or a queue-worker daemon that boots a Workerman worker and does the fan-out inside it, then pushes results into the database or dispatches follow-up jobs. Do not call Eloquent, Redis, or sleep() inside a success callback — it blocks the loop and starves every other in-flight request. Collect into an array, then persist in the completion branch, or dispatch() a job (a fast push onto Redis is usually acceptable; a full Eloquent save of 50 rows is not).
Worker::runAll() with zero workers exits immediately.$response = $client->get(...) — it returns nothing useful. Everything is continuation-passing.try/catch and log; a TypeError in one callback otherwise takes down every other request on that process.getStatusCode() in the success path; check it explicitly. Only transport-level failures (DNS, connect, TLS, read timeout) hit the error callback.max_redirects => 10 chain with a 3s timeout can still burn 30s overall. Lower max_redirects for internal service calls.ssl option (verify_peer/verify_peer_name, or cafile). Don't globally disable verification to "fix" it — pin the CA file.http://user:pass@host:port), not an environment variable — HTTP_PROXY is ignored.getBody() with no exception raised.progress. Multi-hundred-MB downloads will bloat memory per in-flight request.Worker::$logFile / Worker::$stdoutFile and log inside both callbacks with the URL included — the async stack is otherwise invisible. If nothing prints, verify the loop is actually running before touching client options.Client (or wrap it in your own HttpFanout service) to inject logging, retries with backoff, request IDs, and metrics. Retries in particular you must implement yourself: on error, re-issue the request with a usleep-free backoff driven by a Workerman timer, not sleep().How can I help you explore Laravel packages today?