Anankke/SSPanel-UIM · error · Exception
$msg_response->getBody()->getContents()
Error message
$msg_response->getBody()->getContents()
What it means
This line is broken twice: the Postmark response lives in $res, but the throw reads $msg_response, a variable that does not exist in this scope, and the class never imports Exception (no "use Exception;"), so "new Exception" resolves to the non-existent App\Services\Mail\Exception. The intended trigger is a non-200 from POST https://api.postmarkapp.com/email (401 bad X-Postmark-Server-Token, 422 unprocessable recipient/From, 5xx), but the moment that branch runs PHP first warns about the undefined variable and then fatals with "Call to a member function getBody() on null" — you never see Postmark's real error text.
Source
Thrown at src/Services/Mail/Postmark.php:31
{
$configs = Config::getClass('email');
$client = new Client();
$res = $client->post('https://api.postmarkapp.com/email', [
'headers' => [
'Content-Type' => 'application/json',
'X-Postmark-Server-Token' => $configs['postmark_key'],
],
'json' => [
'From' => $configs['postmark_sender'],
'To' => $to,
'Subject' => $subject,
'HtmlBody' => $body,
'MessageStream' => $configs['postmark_stream'],
],
]);
if ($res->getStatusCode() !== 200) {
throw new Exception($msg_response->getBody()->getContents());
}
}
}
View on GitHub (pinned to d55a607191)
Solutions
- Fix the variable: replace $msg_response with $res in the throw statement.
- Add "use Exception;" to the imports in src/Services/Mail/Postmark.php, otherwise PHP looks for App\Services\Mail\Exception and errors with "Class not found".
- Once the throw works, act on Postmark's body: ErrorCode 10 (401) means re-copy the Server Token from Postmark -> Settings -> API tokens; ErrorCode 300/other 422s mean the recipient is on the recipient suppression list (inactive/bounced) or From is not a verified sender.
- Verify the postmark_sender address belongs to a domain with a confirmed DKIM/SPF signature in Postmark, and that postmark_stream matches an existing message stream (default is 'outbound').
- Add a test that mocks a 401 Postmark response to prove the failure path returns the API's message instead of a PHP Error.
Example fix
// before (src/Services/Mail/Postmark.php)
use App\Models\Config;
use GuzzleHttp\Client;
// ...
if ($res->getStatusCode() !== 200) {
throw new Exception($msg_response->getBody()->getContents());
}
// after
use App\Models\Config;
use Exception;
use GuzzleHttp\Client;
// ...
if ($res->getStatusCode() !== 200) {
$err = json_decode((string) $res->getBody(), true) ?: [];
throw new Exception(sprintf('Postmark %d (ErrorCode %s): %s',
$res->getStatusCode(), $err['ErrorCode'] ?? '?', $err['Message'] ?? 'unknown'));
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate mail inputs before calling Postmark::send()
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid recipient address: {$to}");
}
if (!filter_var($configs['postmark_sender'] ?? '', FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('postmark_sender config is not a valid email');
}
if (($configs['postmark_key'] ?? '') === '') {
throw new InvalidArgumentException('postmark_key config is empty');
} Try / catch
// Until the $msg_response -> $res bug and missing "use Exception;" are fixed,
// the failure path throws \Error (call on null / class not found), not Exception:
try {
(new App\Services\Mail\Postmark())->send($to, $subject, $body);
} catch (\Error $e) {
// PHP fatal from the broken throw line — "Call to a member function getBody() on null"
$log->error('Postmark send crashed (known bug): ' . $e->getMessage());
} catch (\Throwable $e) {
$log->error('Mail send failed: ' . $e->getMessage());
} Prevention
- Patch the source first: use $res (not $msg_response) in the throw and add "use Exception;" — no caller-side guard fixes a fatal Error cleanly.
- Verify the sender domain in Postmark (DKIM/return-path confirmed) and use a verified postmark_sender address.
- Validate recipient addresses with filter_var(FILTER_VALIDATE_EMAIL) before sending to avoid predictable 422s.
- Watch Postmark's ErrorCode in response bodies (10 = unauthorized, 300s = recipient suppressed) and disable the mail driver with an admin alert on 401 instead of retrying.
- Add a failure-path unit test with a mocked 401 response so a broken throw line can never ship unnoticed again.
When it happens
Trigger: Any Postmark response with a status other than 200 enters the if-branch and detonates the bug: 401 {"ErrorCode":10,"Message":"Unauthorized"} from a wrong/expired postmark_key config; 422 {"ErrorCode":300,...} for an inactive/invalid recipient email or an unverified From sender domain; 500 from a Postmark incident. Each condition that should raise a readable exception instead crashes with an undefined-variable/undefined-class Error at this exact line.
Common situations: Developer copies the throw line from the Discord notifier (where the variable is $msg_response) into Postmark and never exercises the failure path in tests, so the typo ships; Postmark server token rotated or the sender domain's DKIM/return-path not yet verified, so the first real delivery failure exposes the fatal instead of the message.
Related errors
- $msg_response->getBody()->getContents()
- $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/d90823eb1c5fbf5b.
Report an issue: GitHub.