discord-php/http
Async PHP HTTP client for the Discord REST API. Built for PHP 7.4+ with an event loop and PSR-3 logging. Supports common HTTP verbs, queued requests, JSON-decoded responses, and endpoint constants with parameter binding for correct rate-limit buckets.
composer require discord-php/http
composer require monolog/monolog
use Discord\Http\Http;
use Discord\Http\Drivers\React;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$loop = \React\EventLoop\Factory::create();
$logger = (new Logger('discord_logger'))->pushHandler(new StreamHandler(storage_path('logs/discord.log')));
$http = new Http(
config('services.discord.token'), // Your bot token
$loop,
$logger
);
$http->setDriver(new React($loop));
$loop->run();
use Discord\Http\Endpoint;
// Bind endpoint parameters
$endpoint = Endpoint::bind(Endpoint::APPLICATION, config('services.discord.client_id'));
// Make the request
$http->get($endpoint)->done(
function ($response) {
// Handle success (e.g., store in cache or log)
\Log::info('Bot info:', $response);
},
function ($error) {
\Log::error('Failed to fetch bot info:', ['error' => $error->getMessage()]);
}
);
Wrap the HTTP client in a Laravel command to avoid blocking the CLI process:
use Illuminate\Console\Command;
use React\EventLoop\LoopInterface;
class DiscordSyncCommand extends Command
{
protected $signature = 'discord:sync';
protected $description = 'Sync Discord data asynchronously';
public function handle()
{
$loop = \React\EventLoop\Factory::create();
$http = app(DiscordHttpService::class)->getClient($loop);
// Queue requests
$http->get(Endpoint::APPLICATION)->done(
fn($res) => $this->info('Synced bot info'),
fn($err) => $this->error($err->getMessage())
);
$loop->run();
}
}
Key: Use artisan discord:sync in a cron job or Laravel scheduler.
Group requests by rate-limit bucket using Endpoint::bind():
// Batch channel messages (same bucket)
$channelId = '123456789';
$messageIds = ['msg1', 'msg2', 'msg3'];
$requests = array_map(
fn($id) => $http->get(Endpoint::bind(Endpoint::CHANNEL_MESSAGE, $channelId, $id)),
$messageIds
);
// Process all in parallel
$promise = \React\Promise\all($requests);
$promise->done(
fn($responses) => \Log::info('Fetched messages:', $responses),
fn($err) => \Log::error('Batch failed:', $err)
);
Deploy the event loop in an Octane worker:
// app/Providers/AppServiceProvider.php
public function boot()
{
if (config('octane.enabled')) {
$loop = \React\EventLoop\Factory::create();
$http = new Http(
config('services.discord.token'),
$loop,
app(\Psr\Log\LoggerInterface::class)
);
$http->setDriver(new React($loop));
// Expose HTTP client to Octane workers
app()->singleton(DiscordHttpService::class, fn() => new DiscordHttpService($http, $loop));
}
}
Key: Use app(DiscordHttpService::class) in Octane workers to make requests without blocking HTTP routes.
Implement exponential backoff for retries:
use Discord\Http\Exceptions\RateLimitException;
$http->get(Endpoint::APPLICATION)->otherwise(function ($error) {
if ($error instanceof RateLimitException) {
$retryAfter = $error->getRetryAfter();
$this->info("Rate limited. Retrying in {$retryAfter} seconds...");
// Schedule retry
\Bus::dispatch(new RetryDiscordRequest($error->getEndpoint(), $retryAfter));
}
});
Process Discord webhook payloads asynchronously:
// routes/web.php
Route::post('/discord/webhook', function () {
return response()->json(['status' => 'queued']);
})->middleware('throttle:60,1');
// app/Listeners/DiscordWebhookListener.php
public function handle($payload)
{
$loop = \React\EventLoop\Factory::create();
$http = app(DiscordHttpService::class)->getClient($loop);
// Process payload asynchronously
$http->post(Endpoint::WEBHOOK_EXECUTE, $payload)->done(
fn() => \Log::info('Webhook processed'),
fn($err) => \Log::error('Webhook failed:', $err)
);
$loop->run();
}
$loop->run() will cause requests to never execute.
Fix: Always run the loop in a dedicated process (e.g., Laravel command, Octane worker, or background job).React\Promise\Timer\Timeout to cancel pending requests:
$promise = $http->get(Endpoint::APPLICATION);
$promise->otherwise(function ($error) {
if ($error instanceof \React\Promise\TimeoutException) {
\Log::warning('Request timed out');
}
});
$promise->otherwise(function () use ($promise) {
$promise->cancel();
});
/channels/123/messages/456) instead of Endpoint::bind() can cause requests to bypass rate-limit tracking.
Fix: Always use Endpoint::bind() for parameterized routes:
// Correct (rate-limited)
$endpoint = Endpoint::bind(Endpoint::CHANNEL_MESSAGE, $channelId, $messageId);
// Incorrect (unlimited)
$endpoint = "channels/{$channelId}/messages/{$messageId}";
X-RateLimit-Reset header:
$http->get($endpoint)->done(
function ($response) use ($http) {
$headers = $http->getLastResponseHeaders();
$reset = $headers['X-RateLimit-Reset'] ?? null;
\Log::debug("Rate limit resets at: {$reset}");
}
);
ext-react). If missing, use the Curl driver (synchronous):
$http->setDriver(new \Discord\Http\Drivers\Curl());
$driver = config('app.env') === 'local' ? new React($loop) : new Curl();
$http->setDriver($driver);
$logger = (new Logger('discord'))
->pushHandler(new StreamHandler(storage_path('logs/discord.log'), Logger::DEBUG));
$http->setDebug(true); // Logs raw requests/responses
$loop->run().
Fix: Use a separate process or Octane worker for async requests:
// app/Jobs/ProcessDiscordRequest.php
public function handle()
{
$loop = \React\EventLoop\Factory::create();
$http = app(DiscordHttpService::class)->getClient($loop);
$http->get(Endpoint::APPLICATION)->done(
fn() => \Log::info('Job completed'),
fn($err) => \Log::error('Job failed:', $err)
);
// Detach the loop from the queue worker
$loop->run();
}
afterCommit hook to ensure database transactions complete before making API calls:
public function
How can I help you explore Laravel packages today?