From 120839213c5bffe3b2200b786a01c843703cb424 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Wed, 12 Aug 2026 12:08:07 -0300 Subject: [PATCH] fix: classify Pinterest content-policy rejections (code 1) instead of surfacing raw JSON (#278) * fix: classify Pinterest content-policy rejections (code 1) instead of surfacing raw JSON * fix: map documented Pinterest 404 responses to ContentPolicy category --- .../Social/PinterestPublishException.php | 26 +++++++++++++++++++ .../Social/PinterestPublishExceptionTest.php | 20 ++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/app/Exceptions/Social/PinterestPublishException.php b/app/Exceptions/Social/PinterestPublishException.php index ca13f92a..bfc4c738 100644 --- a/app/Exceptions/Social/PinterestPublishException.php +++ b/app/Exceptions/Social/PinterestPublishException.php @@ -41,6 +41,32 @@ public static function fromApiResponse(mixed $response): static ); } + // Documented for /pins, /media, and /boards alike (Pinterest's public + // OpenAPI spec: github.com/pinterest/api-description). In our flows + // this means the board (or, when polling media status, the media + // upload) referenced by the request no longer exists or isn't + // accessible to this account. + if ($status === 404) { + return new static( + userMessage: 'Pinterest could not find the selected board. It may have been deleted or you no longer have access to it.', + category: ErrorCategory::ContentPolicy, + platformErrorCode: (string) $status, + rawResponse: $rawResponse, + ); + } + + // Pinterest's own JSON error code 1 ("Sorry! This site doesn't allow + // you to save Pins.") is a content-policy rejection reused from their + // legacy "nopin" crawler-block message, not an HTTP/technical error. + if ($status === 400 && (int) data_get($body, 'code') === 1) { + return new static( + userMessage: "Pinterest rejected this pin. This usually means the content violates Pinterest's content policies (e.g. adult or sexual content).", + category: ErrorCategory::ContentPolicy, + platformErrorCode: (string) $status, + rawResponse: $rawResponse, + ); + } + if ($status === 400 && str_contains(strtolower($rawResponse), 'board')) { return new static( userMessage: 'Invalid board. Please select a valid board.', diff --git a/tests/Unit/Exceptions/Social/PinterestPublishExceptionTest.php b/tests/Unit/Exceptions/Social/PinterestPublishExceptionTest.php index cbfc348c..b3ca5123 100644 --- a/tests/Unit/Exceptions/Social/PinterestPublishExceptionTest.php +++ b/tests/Unit/Exceptions/Social/PinterestPublishExceptionTest.php @@ -34,6 +34,26 @@ ->and($exception->userMessage)->toBe('Not authorized to create pins on this board.'); }); +test('HTTP 404 maps to ContentPolicy category', function () { + $response = Http::response(['code' => 2, 'message' => 'Board not found.'], 404); + $fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test'); + + $exception = PinterestPublishException::fromApiResponse($fakeResponse); + + expect($exception->category)->toBe(ErrorCategory::ContentPolicy) + ->and($exception->userMessage)->toBe('Pinterest could not find the selected board. It may have been deleted or you no longer have access to it.'); +}); + +test('HTTP 400 with Pinterest error code 1 maps to ContentPolicy category', function () { + $response = Http::response(['code' => 1, 'message' => "Sorry! This site doesn't allow you to save Pins."], 400); + $fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test'); + + $exception = PinterestPublishException::fromApiResponse($fakeResponse); + + expect($exception->category)->toBe(ErrorCategory::ContentPolicy) + ->and($exception->userMessage)->toBe("Pinterest rejected this pin. This usually means the content violates Pinterest's content policies (e.g. adult or sexual content)."); +}); + test('HTTP 400 with board in body maps to ContentPolicy category', function () { $response = Http::response(['message' => 'Invalid board_id provided.'], 400); $fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test');