monkeyscloud/monkeyslegion-http-client

Enterprise outbound HTTP client for MonkeysLegion: fluent API, cURL transport with connection pooling, middleware pipeline, retry/backoff, PSR-18 bridge, async pools.

Maintainers

Package info

github.com/MonkeysCloud/MonkeysLegion-HTTP-Client

pkg:composer/monkeyscloud/monkeyslegion-http-client

Statistics

Installs: 8

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-17 03:35 UTC

This package is auto-updated.

Last update: 2026-05-17 03:36:49 UTC


README

Enterprise outbound HTTP client for MonkeysLegion: fluent API, cURL transport with connection pooling, middleware pipeline, retry/backoff, PSR-18 bridge, async pools. Zero external runtime dependencies — just ext-curl and PSR interfaces.

Features

Feature Description
Fluent API $client->withToken($t)->post('/api', $data)
cURL Transport Connection pooling, keep-alive, TLS session reuse, LRU eviction
Middleware Pipeline Retry, Auth, Logging, RateLimit, Timeout — composable chain
Retry with Backoff Constant, linear, exponential, exponential+jitter strategies
PSR-18 Bridge Bidirectional: use as PSR-18 client or wrap any PSR-18 as transport
Async Pool Concurrent curl_multi_* requests with configurable concurrency
Mock Transport Queued responses + request history for testing
Event System RequestSending, ResponseReceived, RequestFailed, RetryAttempted
PHPStan Level 9 Full static analysis compliance, 0 errors

Requirements

  • PHP 8.4+
  • ext-curl
  • psr/http-client ^1.0
  • psr/http-message ^2.0
  • psr/http-factory ^1.1

Installation

composer require monkeyscloud/monkeyslegion-http-client

Quick Start

use MonkeysLegion\HttpClient\HttpClient;
use MonkeysLegion\HttpClient\DTO\ClientConfig;

$client = new HttpClient(new ClientConfig(
    baseUrl: 'https://api.example.com',
    timeout: 15,
));

// GET with query params
$response = $client->get('/users', query: ['page' => '1', 'limit' => '25']);
$users = $response->json();

// POST with JSON body
$response = $client->post('/users', json: [
    'name'  => 'Jorge',
    'email' => 'jorge@monkeys.cloud',
]);

// Fluent builder
$response = $client
    ->withToken($apiToken)
    ->withHeader('X-Request-Id', uniqid())
    ->timeout(5)
    ->post('/orders', ['product_id' => 42, 'qty' => 1]);

// Response helpers
$response->statusCode;    // 201
$response->isOk;          // true
$response->json();        // ['id' => 1, 'name' => 'Jorge']
$response->get('id');     // 1
$response->header('X-RateLimit-Remaining'); // "42"
$response->duration;      // 0.234 (seconds)

Architecture

┌──────────────────────────────────────────────────────┐
│                   HttpClient (fluent)                 │
└──────────────────────┬───────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────┐
│              Middleware Pipeline                      │
│  Retry → RateLimit → Auth → Logging → Timeout → …  │
└──────────────────────┬───────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────┐
│                Transport Layer                       │
│  ┌───────────┐  ┌──────────┐  ┌────────────────┐   │
│  │ CurlTransp│  │MockTransp│  │PsrTranspAdapter│   │
│  │ (pooled)  │  │(testing) │  │(Guzzle bridge) │   │
│  └───────────┘  └──────────┘  └────────────────┘   │
└──────────────────────────────────────────────────────┘

Middleware

Retry

use MonkeysLegion\HttpClient\Middleware\RetryMiddleware;
use MonkeysLegion\HttpClient\Retry\RetryPolicy;
use MonkeysLegion\HttpClient\Enum\BackoffStrategy;
use MonkeysLegion\HttpClient\Pipeline\MiddlewarePipeline;
use MonkeysLegion\HttpClient\Transport\CurlTransport;

$transport = new MiddlewarePipeline(new CurlTransport());
$transport->pipe(new RetryMiddleware(new RetryPolicy(
    maxAttempts: 3,
    backoff: BackoffStrategy::ExponentialJitter,
    baseDelayMs: 100,
    maxDelayMs: 5000,
    retryableStatusCodes: [429, 500, 502, 503, 504],
)));

$client = new HttpClient(transport: $transport);

Authentication

use MonkeysLegion\HttpClient\Middleware\AuthMiddleware;

// Bearer token
$transport->pipe(AuthMiddleware::bearer($token));

// Basic auth
$transport->pipe(AuthMiddleware::basic('user', 'pass'));

// API key header
$transport->pipe(AuthMiddleware::apiKey('sk_live_xxx', 'X-API-Key'));

// Custom callback
$transport->pipe(AuthMiddleware::custom(fn($req) =>
    $req->withHeaders(['X-Signature' => hmac($req->body)])
));

Logging (PSR-3)

use MonkeysLegion\HttpClient\Middleware\LoggingMiddleware;

$transport->pipe(new LoggingMiddleware(
    logger: $psrLogger,
    level: 'debug',
    bodyLimit: 1024, // truncate bodies > 1KB
));
// Automatically redacts Authorization, Cookie, X-API-Key headers

Rate Limiting

use MonkeysLegion\HttpClient\Middleware\RateLimitMiddleware;

$transport->pipe(new RateLimitMiddleware(maxPerSecond: 10));
// Per-domain token bucket

Timeout

use MonkeysLegion\HttpClient\Middleware\TimeoutMiddleware;

$transport->pipe(new TimeoutMiddleware(timeout: 30, connectTimeout: 5));

PSR-18 Bridge

Use HttpClient as PSR-18

use MonkeysLegion\HttpClient\Bridge\PsrClientAdapter;

$psrClient = new PsrClientAdapter($httpClient);
$psrResponse = $psrClient->sendRequest($psrRequest);
// Works as drop-in for any PSR-18 consumer (Stripe, AWS SDK, etc.)

Use Guzzle as Transport

use MonkeysLegion\HttpClient\Bridge\PsrTransportAdapter;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;

$guzzle = new GuzzleClient();
$factory = new HttpFactory();
$transport = new PsrTransportAdapter($guzzle, $factory, $factory);

$client = new HttpClient(transport: $transport);

Async / Concurrent Requests

use MonkeysLegion\HttpClient\Async\Pool;
use MonkeysLegion\HttpClient\DTO\RequestContext;
use MonkeysLegion\HttpClient\Enum\HttpMethod;

$requests = [
    new RequestContext(HttpMethod::GET, 'https://api.example.com/users/1'),
    new RequestContext(HttpMethod::GET, 'https://api.example.com/users/2'),
    new RequestContext(HttpMethod::GET, 'https://api.example.com/users/3'),
];

$responses = Pool::send($transport, $requests, concurrency: 5);
// Returns list<HttpResponse> in same order as requests

Testing

use MonkeysLegion\HttpClient\Transport\MockTransport;
use MonkeysLegion\HttpClient\HttpClient;

$mock = new MockTransport();
$mock->enqueueJson(['id' => 1, 'name' => 'Test'], statusCode: 201);

$client = new HttpClient(transport: $mock);
$response = $client->post('/users', json: ['name' => 'Test']);

assert($response->statusCode === 201);
assert($response->get('name') === 'Test');
assert($mock->requestCount() === 1);
assert($mock->assertSent('/users'));

Configuration

php ml http-client:install
http_client {
    timeout         = ${HTTP_CLIENT_TIMEOUT:-30}
    connect_timeout = ${HTTP_CLIENT_CONNECT_TIMEOUT:-5}
    verify_ssl      = ${HTTP_CLIENT_VERIFY_SSL:-true}
    max_redirects   = ${HTTP_CLIENT_MAX_REDIRECTS:-5}
    user_agent      = ${HTTP_CLIENT_USER_AGENT:-MonkeysLegion/2.0}
    pool_size       = ${HTTP_CLIENT_POOL_SIZE:-10}
    throw_on_error  = ${HTTP_CLIENT_THROW_ON_ERROR:-false}

    retry {
        max_attempts = ${HTTP_CLIENT_RETRY_MAX:-3}
        backoff      = ${HTTP_CLIENT_RETRY_BACKOFF:-exponential_jitter}
        base_delay   = ${HTTP_CLIENT_RETRY_DELAY:-100}
        max_delay    = ${HTTP_CLIENT_RETRY_MAX_DELAY:-5000}
    }
}

Events

Event When
RequestSending Before request is sent
ResponseReceived After response received
RequestFailed On connection/timeout failure
RetryAttempted When retry is triggered

Response API

$response->statusCode;     // int
$response->body;           // string (raw)
$response->headers;        // array<string, string>
$response->duration;       // float (seconds)
$response->isOk;           // 2xx
$response->isRedirect;     // 3xx
$response->isClientError;  // 4xx
$response->isServerError;  // 5xx
$response->isFailed;       // 4xx or 5xx
$response->json();         // array<string, mixed>
$response->get('key');     // dot-notation access
$response->header('name'); // case-insensitive header lookup
$response->toArray();      // full response as array

License

MIT © MonkeysCloud