HTTP Client

Introduction

Slenix includes a fluent HTTP client built on top of PHP's cURL extension. It provides an expressive API for making outgoing HTTP requests to external APIs, webhooks, and microservices — with support for authentication, request bodies, retries, SSL verification, event callbacks, and automatic JSON handling.

The client is available at Slenix\Supports\Http\HttpClient and requires no additional installation or composer packages.

php
use Slenix\Supports\Http\HttpClient;

$response = HttpClient::make()
    ->baseUrl('https://api.example.com/v1')
    ->withAuth('bearer', env('API_TOKEN'))
    ->get('/users');

$users = $response->getBody();

Making Requests

Quick Static Requests

For one-off requests with no additional configuration:

php
use Slenix\Supports\Http\HttpClient;

// Quick GET
$response = HttpClient::quickGet('https://api.github.com/users/octocat');

// Quick POST
$response = HttpClient::quickPost('https://api.example.com/users', [
    'name'  => 'Cláudio',
    'email' => 'claudio@example.com',
]);

GET Requests

php
// Simple GET
$response = HttpClient::make()->get('https://api.example.com/users');

// GET with query parameters
$response = HttpClient::make()
    ->baseUrl('https://api.example.com/v1')
    ->get('/users', ['page' => 2, 'per_page' => 25]);

// URL becomes: https://api.example.com/v1/users?page=2&per_page=25

POST, PUT, PATCH, DELETE

php
$client = HttpClient::make()->baseUrl('https://api.example.com/v1');

// POST
$response = $client->post('/users', ['name' => 'Cláudio']);

// PUT — full replace
$response = $client->put('/users/42', ['name' => 'Updated Name']);

// PATCH — partial update
$response = $client->patch('/users/42', ['email' => 'new@example.com']);

// DELETE
$response = $client->delete('/users/42');

// HEAD / OPTIONS
$response = $client->head('/users');
$response = $client->options('/users');

Generic Request Method

php
$response = HttpClient::make()->request('GET', 'https://api.example.com/ping');

// With options
$response = HttpClient::make()->request('POST', '/users', [
    'body'  => ['name' => 'Cláudio'],
    'query' => ['notify' => 1],
]);

Request Body

JSON Body

Setting a JSON body automatically adds the Content-Type: application/json header:

php
$response = HttpClient::make()
    ->baseUrl('https://api.example.com')
    ->asJson([
        'name'  => 'Cláudio',
        'email' => 'claudio@example.com',
        'role'  => 'admin',
    ])
    ->post('/users');

Form Data (multipart)

Use asForm() for multipart/form-data — ideal for file uploads:

php
$response = HttpClient::make()
    ->asForm([
        'name'     => 'Cláudio',
        'document' => new \CURLFile(
            storage_path('docs/contract.pdf'),
            'application/pdf',
            'contract.pdf'
        ),
    ])
    ->post('https://api.example.com/upload');

URL-Encoded Form

For traditional HTML form submissions:

php
$response = HttpClient::make()
    ->asFormUrlEncoded(['grant_type' => 'client_credentials'])
    ->post('https://auth.example.com/oauth/token');

XML Body

php
$xml = '<?xml version="1.0"?><order><id>123</id></order>';

$response = HttpClient::make()
    ->asXml($xml)
    ->post('https://api.example.com/orders');

Plain Text

php
$response = HttpClient::make()
    ->asText('Hello, World!')
    ->post('https://api.example.com/messages');

Headers

php
// Single header
HttpClient::make()
    ->withHeader('Accept', 'application/json')
    ->get('https://api.example.com/users');

// Multiple headers
HttpClient::make()
    ->withHeaders([
        'Accept'       => 'application/json',
        'X-Request-ID' => uniqid(),
        'X-Version'    => '2',
    ])
    ->get('https://api.example.com/users');

// Remove a header
HttpClient::make()
    ->withHeader('Accept', 'application/json')
    ->withoutHeader('Accept')
    ->get('https://api.example.com/users');

Authentication

Bearer Token (JWT, OAuth2)

php
$response = HttpClient::make()
    ->withAuth('bearer', env('API_TOKEN'))
    ->get('https://api.example.com/profile');

Basic Auth

php
$response = HttpClient::make()
    ->withAuth('basic', ['username', 'password'])
    ->get('https://api.example.com/admin');

Digest Auth

php
$response = HttpClient::make()
    ->withAuth('digest', ['username', 'password'])
    ->get('https://api.example.com/secure');
TypeCredentials FormatDescription
bearerstring tokenSets Authorization: Bearer {token} header
basic[username, password]HTTP Basic Authentication
digest[username, password]HTTP Digest Authentication

Working With Responses

The client returns a Slenix\Http\Response instance. JSON responses are automatically decoded when the server returns Content-Type: application/json:

php
$response = HttpClient::make()
    ->baseUrl('https://api.example.com/v1')
    ->withAuth('bearer', env('API_TOKEN'))
    ->get('/users/42');

// Status code
$code = $response->getStatusCode(); // 200

// Body — array if JSON, string otherwise
$body = $response->getBody();

// Re-send as JSON to the browser
$response->json($body);

// Check for success
if ($response->getStatusCode() === 200) {
    $user = $response->getBody();
    echo $user['name'];
}

Checking Status

php
$response = HttpClient::make()->get('https://api.example.com/status');

if ($response->getStatusCode() === 200) {
    // OK
}

if ($response->getStatusCode() === 404) {
    // Not found
}

if ($response->getStatusCode() >= 500) {
    // Server error
}

Configuration

Creating a Configured Instance

php
$client = HttpClient::make([
    'timeout'          => 15,
    'connect_timeout'  => 5,
    'retries'          => 3,
    'retry_delay'      => 500,    // milliseconds between retries
    'verify'           => true,   // verify SSL certificate
    'follow_redirects' => true,
    'max_redirects'    => 5,
    'user_agent'       => 'MyApp/2.0',
    'http_errors'      => true,   // throw on 4xx/5xx
]);

Fluent Configuration

All configuration methods can be chained:

php
$client = HttpClient::make()
    ->baseUrl('https://api.stripe.com/v1')
    ->withAuth('bearer', env('STRIPE_SECRET'))
    ->withHeader('Stripe-Version', '2024-06-20')
    ->withRetries(3, 500)
    ->timeout(20)
    ->withUserAgent('MyApp/2.0');

Configuration Options

OptionDefaultDescription
timeout30Maximum seconds to wait for a response
connect_timeout5Maximum seconds to wait for connection
retries0Number of retry attempts after failure
retry_delay1000Milliseconds to wait between retries
verifytrueVerify SSL/TLS certificate
follow_redirectstrueFollow HTTP redirects
max_redirects5Maximum number of redirects to follow
user_agentSlenix-HttpClient/1.0User-Agent header
http_errorstrueThrow RuntimeException on 4xx/5xx

Retries

Retries are attempted automatically on any exception (network error, timeout, HTTP error). A configurable delay separates each attempt:

php
$response = HttpClient::make()
    ->baseUrl('https://api.example.com/v1')
    ->withRetries(3, 500) // 3 retries, 500ms apart
    ->timeout(20)
    ->get('/charges');

On the final failed attempt, the exception is re-thrown after firing the error event.


Event Callbacks

Register callbacks for three lifecycle events: before (before each attempt), after (on success), and error (after all retries fail):

php
HttpClient::make()
    ->on('before', function (string $method, string $url, mixed $body) {
        logger()->info("→ {$method} {$url}");
    })
    ->on('after', function (Response $response) {
        logger()->info("← {$response->getStatusCode()}");
    })
    ->on('error', function (\Exception $e) {
        logger()->error("Request failed: " . $e->getMessage());
    })
    ->get('https://api.example.com/users');

Multiple callbacks may be registered for the same event:

php
$client = HttpClient::make()
    ->on('error', fn($e) => logger()->error($e->getMessage()))
    ->on('error', fn($e) => notifySlack($e->getMessage()));
EventParametersTriggered
beforestring $method, string $url, mixed $bodyBefore each request attempt
afterResponse $responseAfter a successful response
error\Exception $eAfter all retry attempts fail

Error Handling

When http_errors is true (the default), responses with 4xx or 5xx status codes throw a RuntimeException:

php
try {
    $response = HttpClient::make(['http_errors' => true])
        ->get('https://api.example.com/missing-resource');
} catch (\RuntimeException $e) {
    if (str_contains($e->getMessage(), '404')) {
        // Handle not found
    }
    if (str_contains($e->getMessage(), '401')) {
        // Token expired — refresh and retry
    }
}

To handle errors manually without exceptions, set http_errors to false and check the status code yourself:

php
$response = HttpClient::make(['http_errors' => false])
    ->get('https://api.example.com/resource');

if ($response->getStatusCode() === 404) {
    return null;
}

if ($response->getStatusCode() >= 500) {
    throw new \RuntimeException('External API unavailable.');
}

Network errors and cURL failures always throw a RuntimeException regardless of the http_errors setting.


Practical Examples

Integrating With a Third-Party API

php
namespace App\Services;

use Slenix\Supports\Http\HttpClient;

class StripeService
{
    private HttpClient $client;

    public function __construct()
    {
        $this->client = HttpClient::make()
            ->baseUrl('https://api.stripe.com/v1')
            ->withAuth('bearer', env('STRIPE_SECRET'))
            ->withHeader('Stripe-Version', '2024-06-20')
            ->withRetries(3, 500)
            ->timeout(20);
    }

    public function listCharges(int $limit = 10): array
    {
        $response = $this->client->get('/charges', ['limit' => $limit]);

        if ($response->getStatusCode() !== 200) {
            throw new \RuntimeException('Stripe API error: ' . $response->getStatusCode());
        }

        return $response->getBody();
    }

    public function createCharge(int $amount, string $currency, string $source): array
    {
        $response = $this->client
            ->asFormUrlEncoded([
                'amount'   => $amount,
                'currency' => $currency,
                'source'   => $source,
            ])
            ->post('/charges');

        return $response->getBody();
    }
}

Sending Webhooks

php
use Slenix\Supports\Http\HttpClient;

function sendWebhook(string $url, array $payload): bool
{
    try {
        $response = HttpClient::make()
            ->withHeader('X-Webhook-Secret', env('WEBHOOK_SECRET'))
            ->withRetries(3, 1000)
            ->timeout(10)
            ->asJson($payload)
            ->post($url);

        return $response->getStatusCode() < 400;

    } catch (\RuntimeException $e) {
        logger()->error("Webhook failed [{$url}]: " . $e->getMessage());
        return false;
    }
}

OAuth2 Client Credentials Flow

php
use Slenix\Supports\Http\HttpClient;

function getAccessToken(): string
{
    $response = HttpClient::make()
        ->asFormUrlEncoded([
            'grant_type'    => 'client_credentials',
            'client_id'     => env('OAUTH_CLIENT_ID'),
            'client_secret' => env('OAUTH_CLIENT_SECRET'),
            'scope'         => 'read write',
        ])
        ->post('https://auth.example.com/oauth/token');

    $body = $response->getBody();
    return $body['access_token'];
}

// Use the token in subsequent requests
$token = getAccessToken();

$response = HttpClient::make()
    ->withAuth('bearer', $token)
    ->get('https://api.example.com/resource');

Integrating With Slenix Routes

php
use Slenix\Http\Routing\Router;
use Slenix\Supports\Http\HttpClient;

Router::get('/github/{user}', function ($request, $response, $params) {
    $github = HttpClient::make()
        ->withHeader('Accept', 'application/vnd.github+json')
        ->withHeader('X-GitHub-Api-Version', '2022-11-28')
        ->withAuth('bearer', env('GITHUB_TOKEN'))
        ->get("https://api.github.com/users/{$params['user']}");

    if ($github->getStatusCode() !== 200) {
        $response->error('GitHub user not found.', 404);
        return;
    }

    $response->json($github->getBody());
});

Logging All Outgoing Requests

Set up a pre-configured client with logging enabled for use across your application:

php
// In a service provider or bootstrap file
function httpClient(): HttpClient
{
    return HttpClient::make()
        ->withRetries(2, 300)
        ->timeout(15)
        ->on('before', function (string $method, string $url) {
            logger()->debug("→ HTTP {$method} {$url}");
        })
        ->on('after', function ($response) {
            logger()->debug("← HTTP " . $response->getStatusCode());
        })
        ->on('error', function (\Exception $e) {
            logger()->error("HTTP error: " . $e->getMessage());
        });
}

// Usage
$response = httpClient()
    ->withAuth('bearer', env('API_TOKEN'))
    ->get('https://api.example.com/data');

Security

The HttpClient is configured with secure defaults. Keep the following points in mind when making requests:

SSL verification is enabled by default (verify => true). Only disable it in local development environments — never in production:

php
// Development only — never in production
HttpClient::make(['verify' => false])->get('https://local-api.test/data');

Sensitive credentials should always be stored in .env and read with env(). Never hardcode tokens or passwords:

php
// ✅ Correct
->withAuth('bearer', env('API_TOKEN'))

// ❌ Wrong — never hardcode secrets
->withAuth('bearer', 'sk_live_abc123')

Timeouts are set by default (timeout: 30, connect_timeout: 5) to prevent outgoing requests from hanging indefinitely.

Retries with delay prevent flooding a failing external service. Use them for transient network errors but not for systematic failures like authentication errors.


Method Reference

MethodReturnsDescription
HttpClient::make(options)selfCreate a new instance
HttpClient::quickGet(url, options)ResponseStatic one-line GET
HttpClient::quickPost(url, data, options)ResponseStatic one-line POST
baseUrl(url)selfSet base URL
withHeader(name, value)selfSet a request header
withHeaders(array)selfSet multiple headers
withoutHeader(name)selfRemove a header
withAuth(type, credentials)selfSet authentication
asJson(data)selfJSON request body
asForm(data)selfMultipart form body
asFormUrlEncoded(data)selfURL-encoded form body
asXml(string)selfXML request body
asText(string)selfPlain text body
withRetries(n, delay)selfConfigure retry behaviour
timeout(seconds)selfRequest timeout
withUserAgent(string)selfSet User-Agent
on(event, callback)selfRegister event callback
get(url, query)ResponseGET request
post(url, data)ResponsePOST request
put(url, data)ResponsePUT request
patch(url, data)ResponsePATCH request
delete(url)ResponseDELETE request
head(url)ResponseHEAD request
options(url)ResponseOPTIONS request
request(method, url, opts)ResponseGeneric request
reset()selfReset state for reuse