diff --git a/app/Services/Social/Concerns/HasSocialHttpClient.php b/app/Services/Social/Concerns/HasSocialHttpClient.php index 092ca717..ed9cd0f8 100644 --- a/app/Services/Social/Concerns/HasSocialHttpClient.php +++ b/app/Services/Social/Concerns/HasSocialHttpClient.php @@ -32,7 +32,7 @@ protected function socialHttp(): PendingRequest return Http::retry( times: 3, sleepMilliseconds: 5000, - when: fn ($exception, $request) => $exception->response?->status() === 429, + when: fn ($exception, $request) => ($exception->response ?? null)?->status() === 429, throw: false, )->timeout(120); } diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index ee925fde..bde45d74 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -236,6 +236,12 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy } $index++; + + // The HTTP client/retry wrapper keeps each segment's body alive + // through reference cycles, so without forcing collection a large + // video accumulates its full size in memory and exhausts the limit. + unset($chunk, $appendResponse); + gc_collect_cycles(); } } finally { fclose($handle); diff --git a/tests/Feature/Services/Social/XPublisherTest.php b/tests/Feature/Services/Social/XPublisherTest.php index ac3b8d2f..434d161a 100644 --- a/tests/Feature/Services/Social/XPublisherTest.php +++ b/tests/Feature/Services/Social/XPublisherTest.php @@ -304,3 +304,52 @@ Http::assertSent(fn ($request) => str_contains($request->url(), '/finalize')); Http::assertSent(fn ($request) => str_contains($request->url(), '/2/tweets')); }); + +test('x publisher uploads a large video in sequential 1MB segments', function () { + $this->post->update([ + 'media' => [ + [ + 'id' => 'test-media-video', + 'path' => 'media/2026-01/big-video.mp4', + 'url' => 'https://example.com/media/2026-01/big-video.mp4', + 'mime_type' => 'video/mp4', + 'original_filename' => 'big-video.mp4', + ], + ], + ]); + + $appendCount = 0; + + Http::fake(function ($request) use (&$appendCount) { + $url = $request->url(); + + if (str_contains($url, '/2/media/upload/initialize')) { + return Http::response(['data' => ['id' => 'media_id_999']], 200); + } + + if (str_contains($url, '/append')) { + $appendCount++; + + return Http::response(null, 204); + } + + if (str_contains($url, '/finalize')) { + return Http::response(['data' => ['id' => 'media_id_999']], 200); + } + + if (str_contains($url, '/2/media/')) { + return Http::response(['processing_info' => ['state' => 'succeeded']], 200); + } + + if (str_contains($url, '/2/tweets')) { + return Http::response(['data' => ['id' => '9876543210987654321', 'text' => 'Hello from X!']], 200); + } + + // ~2.5MB download -> at least three 1MB segments. + return Http::response(str_repeat('x', (int) (2.5 * 1024 * 1024)), 200); + }); + + $this->publisher->publish($this->postPlatform); + + expect($appendCount)->toBeGreaterThan(1); +});