fix(x): poll official media upload STATUS endpoint
Use GET /2/media/upload?media_id=&command=STATUS per X API docs, and persist redacted raw_response on SocialPublishException failures for supportability. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
35e7b703b6
commit
38d74e358d
4 changed files with 61 additions and 14 deletions
|
|
@ -157,6 +157,7 @@ public function handle(): void
|
|||
'failed_at' => now()->toIso8601String(),
|
||||
'content_length' => mb_strlen($this->postPlatform->post->content ?? ''),
|
||||
'media_count' => count($this->postPlatform->post->media ?? []),
|
||||
'raw_response' => $e->context()['raw_response'],
|
||||
]);
|
||||
break;
|
||||
} catch (\Throwable $e) {
|
||||
|
|
|
|||
|
|
@ -361,8 +361,13 @@ private function getMediaCategory(string $mimeType, int $fileSize): ?string
|
|||
private function waitForProcessing(string $mediaId, int $maxAttempts = 20): void
|
||||
{
|
||||
for ($i = 0; $i < $maxAttempts; $i++) {
|
||||
// Official status endpoint: GET /2/media/upload?media_id=...&command=STATUS
|
||||
// (not GET /2/media/{id} — that path is not the upload-status contract).
|
||||
$response = $this->getHttpClient()
|
||||
->get("{$this->baseUrl}/media/{$mediaId}");
|
||||
->get("{$this->baseUrl}/media/upload", [
|
||||
'media_id' => $mediaId,
|
||||
'command' => 'STATUS',
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('X media status check error', ['body' => $this->redactResponseBody($response->body())]);
|
||||
|
|
|
|||
|
|
@ -729,6 +729,7 @@
|
|||
expect($this->postPlatform->error_context['category'])->toBe('permission');
|
||||
expect($this->postPlatform->error_context['platform_error_code'])->toBe('403');
|
||||
expect($this->postPlatform->error_context['content_length'])->toBe(11);
|
||||
expect($this->postPlatform->error_context['raw_response'])->toBe('{"error": "forbidden"}');
|
||||
});
|
||||
|
||||
test('publish to social platform fails when scopes are missing', function () {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,33 @@
|
|||
use App\Services\Media\MediaOptimizer;
|
||||
use App\Services\Social\XPublisher;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Official X upload STATUS: GET /2/media/upload?media_id=...&command=STATUS
|
||||
*/
|
||||
function isXMediaUploadStatusRequest(Request $request): bool
|
||||
{
|
||||
if (strtoupper($request->method()) !== 'GET') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $request->url();
|
||||
|
||||
if (
|
||||
! str_contains($url, '/media/upload')
|
||||
|| str_contains($url, '/initialize')
|
||||
|| str_contains($url, '/append')
|
||||
|| str_contains($url, '/finalize')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains($url, 'media_id=')
|
||||
|| filled(data_get($request->data(), 'media_id'));
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
|
|
@ -258,7 +283,7 @@
|
|||
], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/gif_media_555')) {
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'processing_info' => ['state' => 'succeeded'],
|
||||
|
|
@ -301,8 +326,18 @@
|
|||
return str_contains($contentType, 'application/json')
|
||||
&& $request->body() === '{}';
|
||||
});
|
||||
// waitForProcessing was called
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/2/media/gif_media_555'));
|
||||
// waitForProcessing polls the official STATUS endpoint
|
||||
Http::assertSent(function ($request) {
|
||||
return isXMediaUploadStatusRequest($request)
|
||||
&& (
|
||||
str_contains($request->url(), 'media_id=gif_media_555')
|
||||
|| data_get($request->data(), 'media_id') === 'gif_media_555'
|
||||
)
|
||||
&& (
|
||||
str_contains($request->url(), 'command=STATUS')
|
||||
|| data_get($request->data(), 'command') === 'STATUS'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('x publisher recovers a missing mime type from the downloaded bytes', function () {
|
||||
|
|
@ -585,6 +620,10 @@
|
|||
return Http::response(['data' => ['id' => 'video_media_777']], 200);
|
||||
}
|
||||
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response(['data' => ['processing_info' => ['state' => 'succeeded']]], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/tweets')) {
|
||||
return Http::response(['data' => ['id' => '7778889990', 'text' => 'Hello from X!']], 200);
|
||||
}
|
||||
|
|
@ -627,9 +666,9 @@
|
|||
return Http::response(['data' => ['id' => 'media_id_999']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/')) {
|
||||
// STATUS check: GET /2/media/{id}
|
||||
return Http::response(['processing_info' => ['state' => 'succeeded']], 200);
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
// STATUS check: GET /2/media/upload?media_id=...&command=STATUS
|
||||
return Http::response(['data' => ['processing_info' => ['state' => 'succeeded']]], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/tweets')) {
|
||||
|
|
@ -651,6 +690,7 @@
|
|||
&& (int) data_get($request->data(), 'segment_index') === 0;
|
||||
});
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/finalize'));
|
||||
Http::assertSent(fn ($request) => isXMediaUploadStatusRequest($request));
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), '/2/tweets')) {
|
||||
return false;
|
||||
|
|
@ -692,8 +732,8 @@
|
|||
return Http::response(['data' => ['id' => 'media_id_json']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/')) {
|
||||
return Http::response(['processing_info' => ['state' => 'succeeded']], 200);
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response(['data' => ['processing_info' => ['state' => 'succeeded']]], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/tweets')) {
|
||||
|
|
@ -762,8 +802,8 @@
|
|||
return Http::response(['data' => ['id' => 'media_id_999']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/')) {
|
||||
return Http::response(['processing_info' => ['state' => 'succeeded']], 200);
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response(['data' => ['processing_info' => ['state' => 'succeeded']]], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/tweets')) {
|
||||
|
|
@ -927,7 +967,7 @@
|
|||
return Http::response(['data' => ['id' => 'amplify_1']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/')) {
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response(['data' => ['processing_info' => ['state' => 'succeeded']]], 200);
|
||||
}
|
||||
|
||||
|
|
@ -1058,7 +1098,7 @@
|
|||
], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/media_proc_fail')) {
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'processing_info' => [
|
||||
|
|
@ -1104,7 +1144,7 @@
|
|||
return Http::response(['data' => ['id' => 'media_invalid']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/')) {
|
||||
if (isXMediaUploadStatusRequest($request)) {
|
||||
return Http::response(['data' => ['processing_info' => ['state' => 'succeeded']]], 200);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue