feat: add InstagramPublishException with 25 error codes
This commit is contained in:
parent
c3c51d4ccd
commit
6587c76bc2
2 changed files with 257 additions and 0 deletions
72
app/Exceptions/Social/InstagramPublishException.php
Normal file
72
app/Exceptions/Social/InstagramPublishException.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Social;
|
||||
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class InstagramPublishException extends SocialPublishException
|
||||
{
|
||||
public static function fromApiResponse(mixed $response): static
|
||||
{
|
||||
/** @var Response $response */
|
||||
$body = $response->json();
|
||||
$rawResponse = $response->body();
|
||||
|
||||
$errorType = data_get($body, 'error.type');
|
||||
$errorCode = data_get($body, 'error.code');
|
||||
$errorSubcode = data_get($body, 'error.error_subcode');
|
||||
$errorUserMsg = data_get($body, 'error.error_user_msg');
|
||||
$errorMessage = data_get($body, 'error.message', 'An unknown Instagram error occurred.');
|
||||
|
||||
if ($errorType === 'OAuthException' || $errorCode === 190) {
|
||||
throw new TokenExpiredException(
|
||||
message: $errorMessage,
|
||||
platformErrorCode: $errorCode !== null ? (string) $errorCode : null,
|
||||
);
|
||||
}
|
||||
|
||||
[$message, $category] = match ($errorSubcode) {
|
||||
2207026 => ['Unsupported video format. Please upload MP4 or MOV.', ErrorCategory::MediaFormat],
|
||||
2207005 => ['Unsupported image format.', ErrorCategory::MediaFormat],
|
||||
2207004 => ['Image is too large (max 8MB).', ErrorCategory::MediaFormat],
|
||||
2207009 => ['Aspect ratio not supported (must be between 4:5 and 1.91:1).', ErrorCategory::MediaFormat],
|
||||
2207057 => ['Thumbnail offset is outside the video duration.', ErrorCategory::MediaFormat],
|
||||
2207023 => ['Unknown media type.', ErrorCategory::MediaFormat],
|
||||
2207003 => ['Media download timed out. Please try again.', ErrorCategory::ServerError],
|
||||
2207020 => ['Media has expired. Please upload again.', ErrorCategory::ServerError],
|
||||
2207032 => ['Failed to create media. Please try again.', ErrorCategory::ServerError],
|
||||
2207053 => ['Unknown upload error. Please try again.', ErrorCategory::ServerError],
|
||||
2207052 => ['Could not fetch media from URL. Please try again.', ErrorCategory::ServerError],
|
||||
2207006 => ['Media not found. Please upload again.', ErrorCategory::ServerError],
|
||||
2207008 => ['Media container expired. Please try again in a few minutes.', ErrorCategory::ServerError],
|
||||
2207027 => ['Media is not ready for publishing. Please wait and try again.', ErrorCategory::ServerError],
|
||||
2207001 => ['Instagram server error. Please try again.', ErrorCategory::ServerError],
|
||||
2207010 => ['Caption is too long (max 2,200 characters, 30 hashtags, 20 @mentions).', ErrorCategory::ContentPolicy],
|
||||
2207028 => ['Carousel needs between 2 and 10 photos/videos.', ErrorCategory::ContentPolicy],
|
||||
2207051 => ['Instagram restricted this action to protect the community.', ErrorCategory::ContentPolicy],
|
||||
2207035 => ['Product tag positions are not supported for videos.', ErrorCategory::ContentPolicy],
|
||||
2207036 => ['Product tag positions are required for photos.', ErrorCategory::ContentPolicy],
|
||||
2207037 => ['Invalid product tag. The product may be deleted or not permitted.', ErrorCategory::ContentPolicy],
|
||||
2207040 => ['Too many tags (max 20).', ErrorCategory::ContentPolicy],
|
||||
2207042 => ['Daily publishing limit reached. Please try again tomorrow.', ErrorCategory::RateLimit],
|
||||
2207050 => ['Instagram account is restricted or inactive. Please check the Instagram app.', ErrorCategory::Permission],
|
||||
2207081 => ["This account doesn't support Trial Reels.", ErrorCategory::Permission],
|
||||
default => [$errorUserMsg ?? $errorMessage, ErrorCategory::Unknown],
|
||||
};
|
||||
|
||||
return new static(
|
||||
userMessage: $message,
|
||||
category: $category,
|
||||
platformErrorCode: $errorSubcode !== null ? (string) $errorSubcode : null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public function platform(): string
|
||||
{
|
||||
return 'instagram';
|
||||
}
|
||||
}
|
||||
185
tests/Unit/Exceptions/Social/InstagramPublishExceptionTest.php
Normal file
185
tests/Unit/Exceptions/Social/InstagramPublishExceptionTest.php
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\InstagramPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
test('known subcode 2207026 maps to correct message and MediaFormat category', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Some API message',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 2207026,
|
||||
'is_transient' => false,
|
||||
'error_user_msg' => 'Video format not supported.',
|
||||
'fbtrace_id' => 'abc123',
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception)
|
||||
->toBeInstanceOf(InstagramPublishException::class)
|
||||
->and($exception->userMessage)->toBe('Unsupported video format. Please upload MP4 or MOV.')
|
||||
->and($exception->category)->toBe(ErrorCategory::MediaFormat);
|
||||
});
|
||||
|
||||
test('known subcode 2207042 maps to RateLimit category', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Rate limit hit',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 2207042,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::RateLimit)
|
||||
->and($exception->userMessage)->toBe('Daily publishing limit reached. Please try again tomorrow.');
|
||||
});
|
||||
|
||||
test('known subcode 2207050 maps to Permission category', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Account restricted',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 2207050,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::Permission)
|
||||
->and($exception->userMessage)->toBe('Instagram account is restricted or inactive. Please check the Instagram app.');
|
||||
});
|
||||
|
||||
test('known subcode 2207010 maps to ContentPolicy category', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Caption too long',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 2207010,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::ContentPolicy)
|
||||
->and($exception->userMessage)->toBe('Caption is too long (max 2,200 characters, 30 hashtags, 20 @mentions).');
|
||||
});
|
||||
|
||||
test('OAuthException type throws TokenExpiredException', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Invalid OAuth access token',
|
||||
'type' => 'OAuthException',
|
||||
'code' => 400,
|
||||
'error_subcode' => 463,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
})->throws(TokenExpiredException::class);
|
||||
|
||||
test('error code 190 throws TokenExpiredException', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Invalid access token',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 190,
|
||||
'error_subcode' => 460,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
})->throws(TokenExpiredException::class);
|
||||
|
||||
test('unknown subcode with error_user_msg uses that message', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Generic API message',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 9999999,
|
||||
'error_user_msg' => 'A friendly user-facing message.',
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->userMessage)->toBe('A friendly user-facing message.')
|
||||
->and($exception->category)->toBe(ErrorCategory::Unknown);
|
||||
});
|
||||
|
||||
test('unknown subcode without error_user_msg falls through to error message', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Some generic API error occurred.',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 9999999,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->userMessage)->toBe('Some generic API error occurred.');
|
||||
});
|
||||
|
||||
test('platformErrorCode is set to the subcode string', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Some error',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 2207026,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->platformErrorCode)->toBe('2207026');
|
||||
});
|
||||
|
||||
test('platform returns instagram', function () {
|
||||
$response = Http::response([
|
||||
'error' => [
|
||||
'message' => 'Some error',
|
||||
'type' => 'IGApiException',
|
||||
'code' => 36000,
|
||||
'error_subcode' => 2207001,
|
||||
],
|
||||
], 400);
|
||||
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://graph.instagram.com/test');
|
||||
|
||||
$exception = InstagramPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->platform())->toBe('instagram');
|
||||
});
|
||||
Loading…
Reference in a new issue