diff --git a/app/Exceptions/Social/XPublishException.php b/app/Exceptions/Social/XPublishException.php index 97f8dd18..6cbb1b5f 100644 --- a/app/Exceptions/Social/XPublishException.php +++ b/app/Exceptions/Social/XPublishException.php @@ -47,6 +47,15 @@ public static function fromApiResponse(mixed $response): static ); } + if ($statusCode === 413) { + return new static( + userMessage: 'Media chunk rejected by X (payload too large).', + category: ErrorCategory::MediaFormat, + platformErrorCode: (string) $statusCode, + rawResponse: $rawResponse, + ); + } + if (in_array($statusCode, [500, 502, 503, 504], true)) { return new static( userMessage: 'X server error. Please try again later.', diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index 15aa6ec9..ba9ef292 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -205,8 +205,11 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy throw new \Exception('No media_id returned from INIT'); } - // APPEND - Read from temp file in 5MB chunks (memory-safe) - $chunkSize = 5 * 1024 * 1024; + // APPEND - Read from temp file in 1MB chunks. Matches the + // twitter-api-v2 SDK default and X's own quickstart examples; + // larger chunks (we previously used 5MB) trigger 413 at the X + // edge with an empty body, surfacing as "An unknown X error". + $chunkSize = 1024 * 1024; $handle = fopen($tempFile, 'r'); $index = 0; @@ -220,7 +223,7 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy $appendResponse = $this->socialHttp()->withToken($this->accessToken) ->timeout(300) - ->attach('media', $chunk, 'chunk'.$index) + ->attach('media', $chunk, 'chunk'.$index, ['Content-Type' => $mimeType]) ->post("{$this->baseUrl}/media/upload/{$mediaId}/append", [ 'segment_index' => $index, ]); diff --git a/tests/Unit/Exceptions/Social/XPublishExceptionTest.php b/tests/Unit/Exceptions/Social/XPublishExceptionTest.php index 2001af07..978d7413 100644 --- a/tests/Unit/Exceptions/Social/XPublishExceptionTest.php +++ b/tests/Unit/Exceptions/Social/XPublishExceptionTest.php @@ -77,6 +77,18 @@ ->and($exception->userMessage)->toBe('X server error. Please try again later.'); }); +test('HTTP 413 with empty body maps to MediaFormat category', function () { + $response = Http::response('', 413); + + $fakeResponse = Http::fake(['*' => $response])->post('https://api.x.com/test'); + + $exception = XPublishException::fromApiResponse($fakeResponse); + + expect($exception->category)->toBe(ErrorCategory::MediaFormat) + ->and($exception->userMessage)->toBe('Media chunk rejected by X (payload too large).') + ->and($exception->platformErrorCode)->toBe('413'); +}); + test('unknown type maps to Unknown category with detail as message', function () { $response = Http::response([ 'type' => 'https://api.x.com/2/problems/some-unknown-problem',