feat: add InstagramPublishException with 25 error codes

This commit is contained in:
Paulo Castellano 2026-03-31 20:37:52 -03:00
parent c3c51d4ccd
commit 6587c76bc2
2 changed files with 257 additions and 0 deletions

View 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';
}
}

View 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');
});