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

  1. Fix the variable: replace $msg_response with $res in the throw statement.
  2. 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".
  3. 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.
  4. 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').
  5. 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

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


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