{"record":{"id":"26983c1df66791a0","repo":"Anankke/SSPanel-UIM","slug":"msg-response-getbody-getcontents","errorCode":null,"errorMessage":"$msg_response->getBody()->getContents()","messagePattern":"\\$msg_response->getBody\\(\\)->getContents\\(\\)","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/Services/IM/Discord.php","lineNumber":69,"sourceCode":"                'json' => $dm_body,\n            ]);\n\n            $to = json_decode($dm_response->getBody()->getContents())->id;\n        }\n\n        $channel_url = 'https://discord.com/api/v10/channels/' . $to . '/messages';\n\n        $msg_body = [\n            'content' => $msg,\n        ];\n\n        $msg_response = $this->client->post($channel_url, [\n            'headers' => $headers,\n            'json' => $msg_body,\n        ]);\n\n        if ($msg_response->getStatusCode() !== 200) {\n            throw new Exception($msg_response->getBody()->getContents());\n        }\n    }\n}\n","sourceCodeStart":51,"sourceCodeEnd":73,"githubUrl":"https://github.com/Anankke/SSPanel-UIM/blob/d55a607191cfc51cdbc836fba85196ddef4df343/src/Services/IM/Discord.php#L51-L73","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$msg_response = $this->client->post($channel_url, ['headers' => $headers, 'json' => $msg_body]);\nif ($msg_response->getStatusCode() !== 200) {\n    throw new Exception($msg_response->getBody()->getContents());\n}\n\n// after\n$msg_response = $this->client->post($channel_url, ['headers' => $headers, 'json' => $msg_body]);\n$payload = json_decode((string) $msg_response->getBody(), true) ?: [];\nif ($msg_response->getStatusCode() !== 200 || (int) $msg_response->getStatusCode() !== 200) {\n    $code = $payload['code'] ?? 0;\n    throw new Exception(sprintf('Discord %d (code %d): %s',\n        $msg_response->getStatusCode(), $code, $payload['message'] ?? 'unknown error'));\n}","handlingStrategy":"try-catch","validationCode":"// Cheap pre-flight: token looks like a bot token and message fits Discord's limit\n$token = trim((string) Config::obtain('discord_bot_token'));\nif ($token === '' || !str_starts_with($token, 'MT')) {\n    // modern bot tokens base64-decode from an ID starting with 'MT'; treat others as suspect\n    throw new InvalidArgumentException('discord_bot_token missing or malformed');\n}\nif (mb_strlen($msg) > 2000) {\n    $msg = mb_substr($msg, 0, 1997) . '...';\n}\nif ($to <= 0) {\n    throw new InvalidArgumentException('Discord channel ID must be a positive snowflake');\n}","typeGuard":"function describeDiscordError(string $body): array\n{\n    $json = json_decode($body, true);\n    return is_array($json) && isset($json['code'], $json['message'])\n        ? ['code' => (int) $json['code'], 'message' => $json['message']] // e.g. 50013 Missing Permissions\n        : ['code' => 0, 'message' => $body];\n}","tryCatchPattern":"try {\n    $discord->send($channel, $text);\n} catch (GuzzleHttp\\Exception\\GuzzleException $e) {\n    // 4xx/5xx normally surface here first (Guzzle http_errors default)\n    $resp = $e instanceof GuzzleHttp\\Exception\\RequestException ? $e->getResponse() : null;\n    $info = $resp ? describeDiscordError((string) $resp->getBody()) : ['code' => 0, 'message' => $e->getMessage()];\n    $log->warning('Discord delivery failed', $info);\n} catch (Exception $e) {\n    $info = describeDiscordError($e->getMessage()); // status!=200 branch in Discord::send()\n    if (in_array($info['code'], [50013, 50001], true)) {\n        $log->error('Discord bot lacks permissions for channel ' . $channel);\n    }\n}","preventionTips":["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."],"tags":["php","discord","bot","guzzle","api-error","permissions","notifications"],"backgroundTag":"discord-api-error","analyzedSha":"d55a607191cfc51cdbc836fba85196ddef4df343","analyzedAt":"2026-08-21T04:59:05.849Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}