mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-14 00:33:44 -04:00
fix(email): show Resend errors instead of failing in the error handler
resend-php 0.18 reads a "code" key that the Resend API does not send (it sends statusCode), so every Resend error failed with Undefined array key "code". Errors with names that the SDK does not know were returned as a normal response and counted as a sent email. Read statusCode and name from the error, and treat a response without an email id as an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5f6fb5dcc2
commit
d404de87d7
@@ -44,9 +44,9 @@ afterEach(function () {
|
||||
|
||||
/**
|
||||
* Point the real Resend client at a local HTTP stub that answers every request
|
||||
* with the given Resend error, so the whole Resend SDK error path is exercised.
|
||||
* with the given status and JSON body, so the whole Resend SDK path is exercised.
|
||||
*/
|
||||
function fakeResendError(int $code, string $message): Process
|
||||
function fakeResendResponse(int $status, array $body): Process
|
||||
{
|
||||
$socket = stream_socket_server('tcp://127.0.0.1:0');
|
||||
$port = (int) parse_url('tcp://'.stream_socket_get_name($socket, false), PHP_URL_PORT);
|
||||
@@ -55,15 +55,14 @@ function fakeResendError(int $code, string $message): Process
|
||||
$router = sys_get_temp_dir()."/coolify-resend-stub-{$port}.php";
|
||||
file_put_contents($router, <<<'PHP'
|
||||
<?php
|
||||
$code = (int) getenv('RESEND_STUB_CODE');
|
||||
http_response_code($code);
|
||||
http_response_code((int) getenv('RESEND_STUB_STATUS'));
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => ['message' => getenv('RESEND_STUB_MESSAGE'), 'type' => 'error', 'code' => $code]]);
|
||||
echo getenv('RESEND_STUB_BODY');
|
||||
PHP);
|
||||
|
||||
$server = new Process(
|
||||
[PHP_BINARY, '-S', "127.0.0.1:{$port}", $router],
|
||||
env: ['RESEND_STUB_CODE' => (string) $code, 'RESEND_STUB_MESSAGE' => $message],
|
||||
env: ['RESEND_STUB_STATUS' => (string) $status, 'RESEND_STUB_BODY' => json_encode($body)],
|
||||
);
|
||||
$server->start();
|
||||
|
||||
@@ -77,6 +76,14 @@ function fakeResendError(int $code, string $message): Process
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer with the legacy nested error shape ({"error": {"message", "type", "code"}}).
|
||||
*/
|
||||
function fakeResendError(int $code, string $message): Process
|
||||
{
|
||||
return fakeResendResponse($code, ['error' => ['message' => $message, 'type' => 'error', 'code' => $code]]);
|
||||
}
|
||||
|
||||
it('throws user-friendly error for invalid Resend API key (403)', function () {
|
||||
$this->resendStub = fakeResendError(403, 'API key is invalid.');
|
||||
|
||||
@@ -127,3 +134,34 @@ it('throws generic error with message for unknown error codes', function () {
|
||||
expect(fn () => (new EmailChannel)->send($this->team, $this->notification))
|
||||
->toThrow(Exception::class, 'Failed to send email via Resend: Internal server error.');
|
||||
});
|
||||
|
||||
it('surfaces a friendly message for real Resend error bodies', function (int $status, string $name, string $message, string $exception, string $expected) {
|
||||
$this->resendStub = fakeResendResponse($status, ['statusCode' => $status, 'name' => $name, 'message' => $message]);
|
||||
|
||||
expect(fn () => (new EmailChannel)->send($this->team, $this->notification))
|
||||
->toThrow($exception, $expected);
|
||||
})->with([
|
||||
'invalid api key as returned by the live API (401)' => [401, 'validation_error', 'API key is invalid', NonReportableException::class, 'Invalid Resend API key. Please verify your API key in the Resend dashboard and update it in settings.'],
|
||||
'invalid api key (403)' => [403, 'invalid_api_key', 'API key is invalid', NonReportableException::class, 'Invalid Resend API key. Please verify your API key in the Resend dashboard and update it in settings.'],
|
||||
'restricted api key (401)' => [401, 'restricted_api_key', 'This API key is restricted to only send emails', NonReportableException::class, 'Your Resend API key has restricted permissions. Please use an API key with Full Access permissions.'],
|
||||
'unverified domain (403)' => [403, 'validation_error', 'The example.com domain is not verified.', NonReportableException::class, 'Email validation failed: The example.com domain is not verified.'],
|
||||
'validation error (422)' => [422, 'validation_error', 'Invalid `to` field.', NonReportableException::class, 'Email validation failed: Invalid `to` field.'],
|
||||
'rate limited (429)' => [429, 'rate_limit_exceeded', 'Too many requests.', Exception::class, 'Resend rate limit exceeded. Please try again in a few minutes.'],
|
||||
'monthly quota exceeded (429)' => [429, 'monthly_quota_exceeded', 'You have reached your monthly email quota.', Exception::class, 'Failed to send email via Resend: You have reached your monthly email quota.'],
|
||||
'suspended api key (403)' => [403, 'suspended_api_key', 'This API key has been suspended.', NonReportableException::class, 'Invalid Resend API key. Please verify your API key in the Resend dashboard and update it in settings.'],
|
||||
'missing parameter (422)' => [422, 'missing_required_parameter', 'Missing `from` field.', NonReportableException::class, 'Email validation failed: Missing `from` field.'],
|
||||
]);
|
||||
|
||||
it('does not report success when Resend returns no email id', function () {
|
||||
$this->resendStub = fakeResendResponse(200, ['object' => 'error']);
|
||||
|
||||
expect(fn () => (new EmailChannel)->send($this->team, $this->notification))
|
||||
->toThrow(Exception::class, 'Failed to send email via Resend');
|
||||
});
|
||||
|
||||
it('sends successfully when Resend returns an email id', function () {
|
||||
$this->resendStub = fakeResendResponse(200, ['id' => '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794']);
|
||||
|
||||
expect(fn () => (new EmailChannel)->send($this->team, $this->notification))
|
||||
->not->toThrow(Throwable::class);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user