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

  1. 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.
  2. 401: re-copy the bot token from the Developer Portal -> Bot -> Reset Token into the discord_bot_token config (tokens change on reset).
  3. 403/50013: in Discord, check Server Settings -> Roles/channels and grant the bot View Channel + Send Messages (and no channel override denying them).
  4. 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.
  5. 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

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


AI-assisted analysis of Anankke/SSPanel-UIM@d55a607191 (2026-08-21). Data as JSON: /api/errors/26983c1df66791a0. Report an issue: GitHub.