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.
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:
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
// 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=25POST, PUT, PATCH, DELETE
$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
$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:
$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:
$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:
$response = HttpClient::make()
->asFormUrlEncoded(['grant_type' => 'client_credentials'])
->post('https://auth.example.com/oauth/token');XML Body
$xml = '<?xml version="1.0"?><order><id>123</id></order>';
$response = HttpClient::make()
->asXml($xml)
->post('https://api.example.com/orders');Plain Text
$response = HttpClient::make()
->asText('Hello, World!')
->post('https://api.example.com/messages');Headers
// 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)
$response = HttpClient::make()
->withAuth('bearer', env('API_TOKEN'))
->get('https://api.example.com/profile');Basic Auth
$response = HttpClient::make()
->withAuth('basic', ['username', 'password'])
->get('https://api.example.com/admin');Digest Auth
$response = HttpClient::make()
->withAuth('digest', ['username', 'password'])
->get('https://api.example.com/secure');| Type | Credentials Format | Description |
|---|---|---|
bearer | string token | Sets 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:
$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
$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
$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:
$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
| Option | Default | Description |
|---|---|---|
timeout | 30 | Maximum seconds to wait for a response |
connect_timeout | 5 | Maximum seconds to wait for connection |
retries | 0 | Number of retry attempts after failure |
retry_delay | 1000 | Milliseconds to wait between retries |
verify | true | Verify SSL/TLS certificate |
follow_redirects | true | Follow HTTP redirects |
max_redirects | 5 | Maximum number of redirects to follow |
user_agent | Slenix-HttpClient/1.0 | User-Agent header |
http_errors | true | Throw RuntimeException on 4xx/5xx |
Retries
Retries are attempted automatically on any exception (network error, timeout, HTTP error). A configurable delay separates each attempt:
$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):
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:
$client = HttpClient::make()
->on('error', fn($e) => logger()->error($e->getMessage()))
->on('error', fn($e) => notifySlack($e->getMessage()));| Event | Parameters | Triggered |
|---|---|---|
before | string $method, string $url, mixed $body | Before each request attempt |
after | Response $response | After a successful response |
error | \Exception $e | After all retry attempts fail |
Error Handling
When http_errors is true (the default), responses with 4xx or 5xx status codes throw a RuntimeException:
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:
$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
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
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
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
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:
// 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:
// 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:
// ✅ 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
| Method | Returns | Description |
|---|---|---|
HttpClient::make(options) | self | Create a new instance |
HttpClient::quickGet(url, options) | Response | Static one-line GET |
HttpClient::quickPost(url, data, options) | Response | Static one-line POST |
baseUrl(url) | self | Set base URL |
withHeader(name, value) | self | Set a request header |
withHeaders(array) | self | Set multiple headers |
withoutHeader(name) | self | Remove a header |
withAuth(type, credentials) | self | Set authentication |
asJson(data) | self | JSON request body |
asForm(data) | self | Multipart form body |
asFormUrlEncoded(data) | self | URL-encoded form body |
asXml(string) | self | XML request body |
asText(string) | self | Plain text body |
withRetries(n, delay) | self | Configure retry behaviour |
timeout(seconds) | self | Request timeout |
withUserAgent(string) | self | Set User-Agent |
on(event, callback) | self | Register event callback |
get(url, query) | Response | GET request |
post(url, data) | Response | POST request |
put(url, data) | Response | PUT request |
patch(url, data) | Response | PATCH request |
delete(url) | Response | DELETE request |
head(url) | Response | HEAD request |
options(url) | Response | OPTIONS request |
request(method, url, opts) | Response | Generic request |
reset() | self | Reset state for reuse |