Anankke/SSPanel-UIM · error · Exception
$response->getBody()->getContents()
Error message
$response->getBody()->getContents()
What it means
Slack::send() throws a plain Exception whose message is the raw response body after POSTing to slack.com/api/chat.postMessage returned a status other than 200. Slack's Web API mostly answers 200 even for logical failures (with body {"ok":false,"error":...}), so this specific throw fires only on HTTP-level rejections: 401/403 for token problems, 404 for a wrong endpoint, or 429 when rate limited. That also means an invalid slack_token can silently pass this check when Slack answers 200 with ok:false — check the body, not just the status.
Source
Thrown at src/Services/IM/Slack.php:47
$url = 'https://slack.com/api/chat.postMessage';
$headers = [
'Authorization' => 'Bearer '.$this->token,
'Content-Type' => 'application/json',
];
$body = [
'channel' => $to,
'text' => $msg,
];
$response = $this->client->post($url, [
'headers' => $headers,
'json' => $body,
]);
if ($response->getStatusCode() !== 200) {
throw new Exception($response->getBody()->getContents());
}
}
}
View on GitHub (pinned to d55a607191)
Solutions
- Decode the exception message JSON: {"ok":false,"error":"invalid_auth"} points to the token; {"error":"rate_limited"} points to throttling.
- For auth errors, verify the slack_token config value: OAuth token format (xoxb-...), still active, and issued by the same Slack app — reinstall the app if the token was regenerated.
- Add the chat:write scope to the app's bot token (Slack app config -> OAuth & Permissions) and re-install so the scope takes effect.
- On 429, honour the Retry-After response header and throttle chat.postMessage calls (e.g. queue notifications).
- Because Slack returns 200 with ok:false for logical errors, also parse the response body and fail when ok is false — not only when the status differs from 200.
Example fix
// before
$response = $this->client->post($url, ['headers' => $headers, 'json' => $body]);
if ($response->getStatusCode() !== 200) {
throw new Exception($response->getBody()->getContents());
}
// after
$response = $this->client->post($url, ['headers' => $headers, 'json' => $body]);
$payload = json_decode((string) $response->getBody(), true) ?: [];
if ($response->getStatusCode() !== 200) {
throw new Exception(sprintf('Slack HTTP %d: %s', $response->getStatusCode(), $payload['error'] ?? 'unknown'));
}
if (($payload['ok'] ?? false) !== true) {
throw new Exception('Slack error: ' . ($payload['error'] ?? 'unknown'));
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: token shape and channel ID sanity before Slack::send()
$token = trim((string) Config::obtain('slack_token'));
if ($token === '' || !str_starts_with($token, 'xox')) {
throw new InvalidArgumentException('slack_token missing or not an xoxb-/xoxp- token');
}
if ($to <= 0 && !is_string($to)) {
throw new InvalidArgumentException('Slack channel must be an ID (C...) or name');
}
// Optional authoritative check (1 extra call): does the token work at all?
$probe = $client->post('https://slack.com/api/auth.test', ['headers' => ['Authorization' => 'Bearer ' . $token]]);
if ((json_decode((string) $probe->getBody(), true)['ok'] ?? false) !== true) {
throw new RuntimeException('Slack token rejected by auth.test');
} Type guard
function isSlackRateLimited(Exception $e): bool
{
$json = json_decode($e->getMessage(), true);
return is_array($json) && (($json['error'] ?? null) === 'rate_limited' || ($json['ok'] ?? true) === false);
} Try / catch
try {
$slack->send($channelId, $text);
} catch (GuzzleHttp\Exception\GuzzleException $e) {
$resp = $e instanceof GuzzleHttp\Exception\RequestException ? $e->getResponse() : null;
if ($resp !== null && $resp->getStatusCode() === 429) {
$wait = (int) $resp->getHeaderLine('Retry-After');
// schedule the notification again after $wait seconds instead of dropping it
return defer(fn () => $slack->send($channelId, $text), $wait);
}
throw $e;
} catch (Exception $e) {
$payload = json_decode($e->getMessage(), true);
$log->warning('Slack delivery failed', ['error' => $payload['error'] ?? $e->getMessage()]);
} Prevention
- Remember Slack returns 200 with {"ok":false} for logical errors (invalid_auth, channel_not_found) — also inspect the response body, not just status codes.
- Confirm the app token has chat:write scope and the bot is a member of the target channel before wiring notifications.
- Honour the Retry-After header on 429s; queue bursts rather than posting in tight loops.
- Run auth.test once at startup to detect revoked tokens early with a clear error.
When it happens
Trigger: 401 invalid_auth / permission_denied: slack_token config empty, malformed, revoked, or a rotated token after reinstalling the app; 403 missing_scope: token lacks chat:write (or chat:writecustomize:write for custom sender names); 429 rate_limited: too many chat.postMessage calls, Slack answers 429 with a Retry-After header; 404: someone changed $url away from https://slack.com/api/chat.postMessage; channel_not_found / channel ID typos typically arrive as 200 + ok:false, escaping this check entirely.
Common situations: Legacy xoxb- bot token revoked when the Slack app was reinstalled or converted to granular permissions without the chat:write scope; slack_token left blank in panel config; heavy notification storms (mass user notifications) tripping Slack's tiered rate limits; sending to channels the bot was never invited to.
Related errors
- $msg_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/f4e26b8490d04480.
Report an issue: GitHub.