fix: retry LinkedIn publish with token refresh when token is revoked

This commit is contained in:
Paulo Castellano 2026-01-26 10:48:20 -03:00
parent a976e11263
commit e1ac9f87f8
4 changed files with 119 additions and 24 deletions

View file

@ -30,18 +30,23 @@ class LinkedInPagePublisher
private string $accessToken;
private SocialAccount $account;
private bool $hasRetried = false;
public function publish(PostPlatform $postPlatform): array
{
$account = $postPlatform->socialAccount;
$this->account = $postPlatform->socialAccount;
$this->hasRetried = false;
if ($account->is_token_expired || $account->is_token_expiring_soon) {
$this->refreshToken($account);
$account->refresh();
if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) {
$this->refreshToken($this->account);
$this->account->refresh();
}
$this->accessToken = $account->access_token;
$this->accessToken = $this->account->access_token;
$organizationId = $account->meta['organization_id'] ?? null;
$organizationId = $this->account->meta['organization_id'] ?? null;
if (! $organizationId) {
throw new \Exception('LinkedIn Page organization ID not configured');
@ -50,11 +55,50 @@ public function publish(PostPlatform $postPlatform): array
$organizationUrn = "urn:li:organization:{$organizationId}";
$contentType = $postPlatform->content_type;
return match ($contentType) {
ContentType::LinkedInPageCarousel => $this->publishCarousel($organizationUrn, $postPlatform->content, $postPlatform->media, $account),
ContentType::LinkedInPagePost => $this->publishPost($organizationUrn, $postPlatform->content, $postPlatform->media, $account),
default => throw new \Exception("Unsupported LinkedIn Page content type: {$contentType?->value}"),
};
try {
return match ($contentType) {
ContentType::LinkedInPageCarousel => $this->publishCarousel($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
ContentType::LinkedInPagePost => $this->publishPost($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
default => throw new \Exception("Unsupported LinkedIn Page content type: {$contentType?->value}"),
};
} catch (TokenExpiredException $e) {
return $this->retryWithRefresh($postPlatform, $e);
}
}
private function retryWithRefresh(PostPlatform $postPlatform, TokenExpiredException $originalException): array
{
if ($this->hasRetried) {
throw $originalException;
}
$this->hasRetried = true;
Log::info('LinkedIn Page token rejected, attempting refresh and retry', [
'account_id' => $this->account->id,
]);
try {
$this->refreshToken($this->account);
$this->account->refresh();
$this->accessToken = $this->account->access_token;
$organizationId = $this->account->meta['organization_id'] ?? null;
$organizationUrn = "urn:li:organization:{$organizationId}";
$contentType = $postPlatform->content_type;
return match ($contentType) {
ContentType::LinkedInPageCarousel => $this->publishCarousel($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
ContentType::LinkedInPagePost => $this->publishPost($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
default => throw new \Exception("Unsupported LinkedIn Page content type: {$contentType?->value}"),
};
} catch (\Throwable $e) {
Log::error('LinkedIn Page refresh failed during retry', [
'account_id' => $this->account->id,
'error' => $e->getMessage(),
]);
throw $originalException;
}
}
private function publishPost(string $organizationUrn, string $content, $media, $account): array

View file

@ -30,25 +30,68 @@ class LinkedInPublisher
private string $accessToken;
private SocialAccount $account;
private bool $hasRetried = false;
public function publish(PostPlatform $postPlatform): array
{
$account = $postPlatform->socialAccount;
$this->account = $postPlatform->socialAccount;
$this->hasRetried = false;
if ($account->is_token_expired || $account->is_token_expiring_soon) {
$this->refreshToken($account);
$account->refresh();
if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) {
$this->refreshToken($this->account);
$this->account->refresh();
}
$this->accessToken = $account->access_token;
$this->accessToken = $this->account->access_token;
$personUrn = "urn:li:person:{$account->platform_user_id}";
$personUrn = "urn:li:person:{$this->account->platform_user_id}";
$contentType = $postPlatform->content_type;
return match ($contentType) {
ContentType::LinkedInCarousel => $this->publishCarousel($personUrn, $postPlatform->content, $postPlatform->media),
ContentType::LinkedInPost => $this->publishPost($personUrn, $postPlatform->content, $postPlatform->media),
default => throw new \Exception("Unsupported LinkedIn content type: {$contentType?->value}"),
};
try {
return match ($contentType) {
ContentType::LinkedInCarousel => $this->publishCarousel($personUrn, $postPlatform->content, $postPlatform->media),
ContentType::LinkedInPost => $this->publishPost($personUrn, $postPlatform->content, $postPlatform->media),
default => throw new \Exception("Unsupported LinkedIn content type: {$contentType?->value}"),
};
} catch (TokenExpiredException $e) {
return $this->retryWithRefresh($postPlatform, $e);
}
}
private function retryWithRefresh(PostPlatform $postPlatform, TokenExpiredException $originalException): array
{
if ($this->hasRetried) {
throw $originalException;
}
$this->hasRetried = true;
Log::info('LinkedIn token rejected, attempting refresh and retry', [
'account_id' => $this->account->id,
]);
try {
$this->refreshToken($this->account);
$this->account->refresh();
$this->accessToken = $this->account->access_token;
$personUrn = "urn:li:person:{$this->account->platform_user_id}";
$contentType = $postPlatform->content_type;
return match ($contentType) {
ContentType::LinkedInCarousel => $this->publishCarousel($personUrn, $postPlatform->content, $postPlatform->media),
ContentType::LinkedInPost => $this->publishPost($personUrn, $postPlatform->content, $postPlatform->media),
default => throw new \Exception("Unsupported LinkedIn content type: {$contentType?->value}"),
};
} catch (\Throwable $e) {
Log::error('LinkedIn refresh failed during retry', [
'account_id' => $this->account->id,
'error' => $e->getMessage(),
]);
throw $originalException;
}
}
private function publishPost(string $personUrn, string $content, $media): array

View file

@ -115,12 +115,16 @@
->toThrow(Exception::class);
});
test('linkedin page publisher throws token expired exception on auth error', function () {
test('linkedin page publisher throws token expired exception on auth error after retry', function () {
Http::fake([
'https://api.linkedin.com/rest/posts' => Http::response([
'code' => 'EXPIRED_ACCESS_TOKEN',
'message' => 'The token used in the request has expired',
], 401),
'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
'error' => 'invalid_grant',
'error_description' => 'The refresh token is invalid',
], 400),
]);
expect(fn () => $this->publisher->publish($this->postPlatform))

View file

@ -89,12 +89,16 @@
->toThrow(Exception::class);
});
test('linkedin publisher throws token expired exception on auth error', function () {
test('linkedin publisher throws token expired exception on auth error after retry', function () {
Http::fake([
'https://api.linkedin.com/rest/posts' => Http::response([
'code' => 'EXPIRED_ACCESS_TOKEN',
'message' => 'The token used in the request has expired',
], 401),
'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
'error' => 'invalid_grant',
'error_description' => 'The refresh token is invalid',
], 400),
]);
expect(fn () => $this->publisher->publish($this->postPlatform))