Anankke/SSPanel-UIM · error · Exception
$msg_response->getBody()->getContents()
Error message
$msg_response->getBody()->getContents()
What it means
Discord::send() throws a plain Exception whose message is the raw response body after POSTing a message to /channels/{id}/messages returned a status other than 200. The body is Discord's JSON error such as {"message": "Missing Permissions", "code": 50013}, so the exception message is that JSON string. Note that with Guzzle's default http_errors=true, most 4xx/5xx responses actually throw a Guzzle ClientException inside the post() call before this manual check runs — this throw is reached for the remaining non-200 responses.
Source
Thrown at src/Services/IM/Discord.php:69
'json' => $dm_body,
]);
$to = json_decode($dm_response->getBody()->getContents())->id;
}
$channel_url = 'https://discord.com/api/v10/channels/' . $to . '/messages';
$msg_body = [
'content' => $msg,
];
$msg_response = $this->client->post($channel_url, [
'headers' => $headers,
'json' => $msg_body,
]);
if ($msg_response->getStatusCode() !== 200) {
throw new Exception($msg_response->getBody()->getContents());
}
}
}
View on GitHub (pinned to d55a607191)
Solutions
- Read the JSON inside the exception message: the "code" field (50013, 10003, 401) maps to a specific Discord error and tells you whether it is auth, permission, or a wrong channel.
- 401: re-copy the bot token from the Developer Portal -> Bot -> Reset Token into the discord_bot_token config (tokens change on reset).
- 403/50013: in Discord, check Server Settings -> Roles/channels and grant the bot View Channel + Send Messages (and no channel override denying them).
- 404/10003: verify $to is a real channel ID (enable Developer Mode -> right-click channel -> Copy ID) and that the bot is still in that server.
- Truncate notification text to <= 2000 characters before send() to avoid 400 Invalid Form Body; catch GuzzleException too since 4xx/5xx usually surface there first.
Example fix
// before
$msg_response = $this->client->post($channel_url, ['headers' => $headers, 'json' => $msg_body]);
if ($msg_response->getStatusCode() !== 200) {
throw new Exception($msg_response->getBody()->getContents());
}
// after
$msg_response = $this->client->post($channel_url, ['headers' => $headers, 'json' => $msg_body]);
$payload = json_decode((string) $msg_response->getBody(), true) ?: [];
if ($msg_response->getStatusCode() !== 200 || (int) $msg_response->getStatusCode() !== 200) {
$code = $payload['code'] ?? 0;
throw new Exception(sprintf('Discord %d (code %d): %s',
$msg_response->getStatusCode(), $code, $payload['message'] ?? 'unknown error'));
} Defensive patterns
Strategy: try-catch
Validate before calling
// Cheap pre-flight: token looks like a bot token and message fits Discord's limit
$token = trim((string) Config::obtain('discord_bot_token'));
if ($token === '' || !str_starts_with($token, 'MT')) {
// modern bot tokens base64-decode from an ID starting with 'MT'; treat others as suspect
throw new InvalidArgumentException('discord_bot_token missing or malformed');
}
if (mb_strlen($msg) > 2000) {
$msg = mb_substr($msg, 0, 1997) . '...';
}
if ($to <= 0) {
throw new InvalidArgumentException('Discord channel ID must be a positive snowflake');
} Type guard
function describeDiscordError(string $body): array
{
$json = json_decode($body, true);
return is_array($json) && isset($json['code'], $json['message'])
? ['code' => (int) $json['code'], 'message' => $json['message']] // e.g. 50013 Missing Permissions
: ['code' => 0, 'message' => $body];
} Try / catch
try {
$discord->send($channel, $text);
} catch (GuzzleHttp\Exception\GuzzleException $e) {
// 4xx/5xx normally surface here first (Guzzle http_errors default)
$resp = $e instanceof GuzzleHttp\Exception\RequestException ? $e->getResponse() : null;
$info = $resp ? describeDiscordError((string) $resp->getBody()) : ['code' => 0, 'message' => $e->getMessage()];
$log->warning('Discord delivery failed', $info);
} catch (Exception $e) {
$info = describeDiscordError($e->getMessage()); // status!=200 branch in Discord::send()
if (in_array($info['code'], [50013, 50001], true)) {
$log->error('Discord bot lacks permissions for channel ' . $channel);
}
} Prevention
- Catch both GuzzleException and Exception around send() — Guzzle's http_errors throws before the manual status check in most failure cases.
- Parse the JSON inside the exception message and branch on Discord's numeric code (401 auth, 50013 permissions, 10003 unknown channel).
- Truncate notification bodies to 2000 characters before sending.
- After inviting or reconfiguring the bot, send one test message per target channel and alert an admin on failure so permission drift is caught early.
When it happens
Trigger: 401 {"message":"401: Unauthorized"}: invalid/expired discord_bot_token; 403 Missing Permissions (50013): bot lacks SEND_MESSAGES (or VIEW_CHANNEL) in the target channel; 404 Unknown Channel (10003): $to is not a channel the bot can see — e.g. a raw user ID passed where the earlier DM-creation fallback also failed, or the bot was kicked from the guild; 400 Invalid Form Body: message content over 2000 chars or malformed; 429 RATE_LIMITED after message bursts.
Common situations: Bot token regenerated in the Discord Developer Portal but discord_bot_token config not updated; bot invited without the Send Messages permission or with channel-level permission overrides denying it; panel sending notifications to a user ID instead of a channel ID, or to a channel in a guild the bot was removed from; notification text exceeding Discord's 2000-character message limit.
Related errors
- $response->getBody()->getContents()
- $msg_response->getBody()->getContents()
- curl_error($curl)
- json_last_error_msg()
AI-assisted analysis of Anankke/SSPanel-UIM@d55a607191 (2026-08-21).
Data as JSON: /api/errors/26983c1df66791a0.
Report an issue: GitHub.