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).
90 lines
3.3 KiB
PHP
90 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Exceptions\Social;
|
|
|
|
use App\Exceptions\TokenExpiredException;
|
|
use Illuminate\Http\Client\Response;
|
|
|
|
class XPublishException extends SocialPublishException
|
|
{
|
|
public static function fromApiResponse(mixed $response): static
|
|
{
|
|
/** @var Response $response */
|
|
$body = $response->json();
|
|
$rawResponse = $response->body();
|
|
$statusCode = $response->status();
|
|
|
|
$type = data_get($body, 'type', '');
|
|
$title = data_get($body, 'title', '');
|
|
$detail = data_get($body, 'detail', $title);
|
|
|
|
$typeSuffix = $type !== '' ? basename((string) $type) : '';
|
|
|
|
if ($statusCode === 401 || str_contains((string) $type, 'unsupported-authentication')) {
|
|
throw new TokenExpiredException(
|
|
message: $detail ?: 'Access token has expired or been revoked',
|
|
platformErrorCode: $typeSuffix ?: (string) $statusCode,
|
|
);
|
|
}
|
|
|
|
if (str_contains((string) $rawResponse, 'invalid URL')) {
|
|
return new static(
|
|
userMessage: 'Post contains an invalid URL.',
|
|
category: ErrorCategory::ContentPolicy,
|
|
platformErrorCode: $typeSuffix ?: null,
|
|
rawResponse: $rawResponse,
|
|
);
|
|
}
|
|
|
|
if (str_contains((string) $rawResponse, 'video longer than 2 minutes')) {
|
|
return new static(
|
|
userMessage: 'Video exceeds the 2-minute limit.',
|
|
category: ErrorCategory::MediaFormat,
|
|
platformErrorCode: $typeSuffix ?: null,
|
|
rawResponse: $rawResponse,
|
|
);
|
|
}
|
|
|
|
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.',
|
|
category: ErrorCategory::ServerError,
|
|
platformErrorCode: (string) $statusCode,
|
|
rawResponse: $rawResponse,
|
|
);
|
|
}
|
|
|
|
[$message, $category] = match ($typeSuffix) {
|
|
'usage-capped' => ['Usage limit exceeded. Please try again later.', ErrorCategory::RateLimit],
|
|
'rate-limit-exceeded' => ['Rate limit exceeded. Please try again later.', ErrorCategory::RateLimit],
|
|
'invalid-request' => ['Invalid request. Check your post content.', ErrorCategory::ContentPolicy],
|
|
'client-forbidden' => ['App not enrolled or lacks required access.', ErrorCategory::Permission],
|
|
'not-authorized-for-resource' => ['Not authorized for this resource.', ErrorCategory::Permission],
|
|
'resource-not-found' => ['Resource not found.', ErrorCategory::ContentPolicy],
|
|
default => [$detail ?: $title ?: 'An unknown X error occurred.', ErrorCategory::Unknown],
|
|
};
|
|
|
|
return new static(
|
|
userMessage: $message,
|
|
category: $category,
|
|
platformErrorCode: $typeSuffix ?: null,
|
|
rawResponse: $rawResponse,
|
|
);
|
|
}
|
|
|
|
public function platform(): string
|
|
{
|
|
return 'x';
|
|
}
|
|
}
|