diff --git a/app/Services/Social/FacebookPublisher.php b/app/Services/Social/FacebookPublisher.php index 14ecdcb1..abeabbf8 100644 --- a/app/Services/Social/FacebookPublisher.php +++ b/app/Services/Social/FacebookPublisher.php @@ -5,6 +5,7 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; +use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\FacebookPublishException; use App\Models\PostPlatform; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -220,10 +221,6 @@ private function publishReel(string $pageId, string $accessToken, ?string $conte ]); if ($startResponse->failed()) { - Log::error('Facebook reel upload start failed', [ - 'status' => $startResponse->status(), - 'body' => $this->redactResponseBody($startResponse->body()), - ]); $this->handleApiError($startResponse); } @@ -232,10 +229,12 @@ private function publishReel(string $pageId, string $accessToken, ?string $conte $uploadUrl = data_get($startData, 'upload_url'); if (! $videoId || ! $uploadUrl) { - Log::error('Facebook reel start did not return video_id/upload_url', [ - 'body' => $this->redactResponseBody($startResponse->body()), - ]); - $this->handleApiError($startResponse); + throw new FacebookPublishException( + userMessage: 'Facebook did not return upload_url for reel start.', + category: ErrorCategory::ServerError, + platformErrorCode: null, + rawResponse: $startResponse->body(), + ); } // Phase 2 (transfer, local-file flow) — download our hosted @@ -252,29 +251,39 @@ private function publishReel(string $pageId, string $accessToken, ?string $conte ->get($media->url); if ($download->failed()) { - throw new \Exception('Failed to download reel media: HTTP '.$download->status()); + throw new FacebookPublishException( + userMessage: 'Could not download media for Facebook reel.', + category: ErrorCategory::ServerError, + platformErrorCode: (string) $download->status(), + rawResponse: null, + ); } $fileSize = filesize($tempFile); + $stream = fopen($tempFile, 'rb'); - $uploadResponse = Http::withHeaders([ - 'Authorization' => "OAuth {$accessToken}", - 'Offset' => '0', - 'file_size' => (string) $fileSize, - ]) - ->timeout(600) - ->withBody(file_get_contents($tempFile), $media->mime_type ?? 'video/mp4') - ->post($uploadUrl); + try { + $uploadResponse = Http::withHeaders([ + 'Authorization' => "OAuth {$accessToken}", + 'Offset' => '0', + 'file_size' => (string) $fileSize, + ]) + ->timeout(600) + ->withBody($stream, $media->mime_type ?? 'video/mp4') + ->post($uploadUrl); + } finally { + if (is_resource($stream)) { + fclose($stream); + } + } if ($uploadResponse->failed()) { - Log::error('Facebook reel upload transfer failed', [ - 'status' => $uploadResponse->status(), - 'body' => $this->redactResponseBody($uploadResponse->body()), - ]); $this->handleApiError($uploadResponse); } } finally { - @unlink($tempFile); + if (! unlink($tempFile)) { + Log::warning('Facebook reel temp file cleanup failed', ['path' => $tempFile]); + } } // Phase 3 (finish) — publish the reel. @@ -287,9 +296,6 @@ private function publishReel(string $pageId, string $accessToken, ?string $conte ]); if ($finishResponse->failed()) { - Log::error('Facebook reel finish failed', [ - 'body' => $this->redactResponseBody($finishResponse->body()), - ]); $this->handleApiError($finishResponse); } diff --git a/tests/Feature/Services/Social/FacebookPublisherTest.php b/tests/Feature/Services/Social/FacebookPublisherTest.php index 7f817d68..8e039512 100644 --- a/tests/Feature/Services/Social/FacebookPublisherTest.php +++ b/tests/Feature/Services/Social/FacebookPublisherTest.php @@ -233,7 +233,43 @@ ]); expect(fn () => $this->publisher->publish($this->postPlatform)) - ->toThrow(FacebookPublishException::class); + ->toThrow( + FacebookPublishException::class, + 'Facebook did not return upload_url for reel start.' + ); +}); + +test('facebook publisher fails reel publish with typed exception when media download fails', function () { + $this->postPlatform->update(['content_type' => ContentType::FacebookReel]); + + $this->post->update([ + 'media' => [ + [ + 'id' => 'test-media-reel', + 'path' => 'media/2026-01/reel.mp4', + 'url' => 'https://example.com/media/2026-01/reel.mp4', + 'mime_type' => 'video/mp4', + 'original_filename' => 'reel.mp4', + ], + ], + ]); + + // start succeeds, but the media URL returns 404 — should surface as + // a typed FacebookPublishException (ServerError) instead of leaking + // a generic Exception that would land in the 'unknown' bucket. + Http::fake([ + '*/page_123/video_reels' => Http::response([ + 'video_id' => 'reel_video_123', + 'upload_url' => 'https://rupload.facebook.com/video-upload/v25.0/reel_video_123', + ], 200), + '*example.com/media/*' => Http::response('', 404), + ]); + + expect(fn () => $this->publisher->publish($this->postPlatform)) + ->toThrow( + FacebookPublishException::class, + 'Could not download media for Facebook reel.' + ); }); test('facebook publisher can publish image story', function () {