fix(x): drop chunked upload chunk size from 5MB to 1MB
Production was returning HTTP 413 with empty body on the first APPEND segment of every video upload to X v2 — surfacing to users as 'An unknown X error occurred.' Empty-body 413 is the classic signature of an edge/CDN rejection: the X gateway is denying the request before X's application code sees it. The X v2 reference docs say 'max chunk size: 5MB', but every canonical reference uses 1MB: - X's official Python quickstart: `chunk_size = 1024 * 1024` - X's official JavaScript quickstart: `const chunkSize = 1024 * 1024` - twitter-api-v2 (the most-used Node SDK, used by Postiz et al.): `chunkSize: number = 1024 * 1024` 5MB plus multipart-form overhead apparently exceeds an undocumented edge limit. 1MB is the empirically safe size everyone converges on. Changes: - XPublisher chunked APPEND now uses 1MB chunks. An 8MB video uploads as 8 segments instead of 2; more roundtrips but actually succeeds. - Set explicit Content-Type on each chunk attach (matches the simple upload path in the same file). - XPublishException::fromApiResponse maps HTTP 413 to ErrorCategory::MediaFormat with the message 'Media chunk rejected by X (payload too large).' so we don't surface 413 as 'unknown' if it ever recurs. Test: unit test covering the 413→MediaFormat mapping. Full suite green (1503 passed, 2 skipped).
This commit is contained in:
parent
6c2c555bd0
commit
7d82cc3f12
3 changed files with 27 additions and 3 deletions
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue