fix(instagram): persist publish state at the publish boundary to prevent duplicates and false failures#1681
fix(instagram): persist publish state at the publish boundary to prevent duplicates and false failures#1681giladresisi wants to merge 1 commit into
Conversation
|
Contribution-checker quality warning Heuristics that flagged:
If this is a genuine contribution, please add detail to your PR description and tighten the diff scope before reviewers look at it. |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
| releaseURL: fallbackUrl, | ||
| status: 'success', | ||
| }); | ||
|
|
||
| lastPermalink = await this.getPermalink( | ||
| type, | ||
| mediaId, | ||
| userToken || accessToken, | ||
| fallbackUrl | ||
| ); | ||
| } |
There was a problem hiding this comment.
Bug: A partial failure during a multi-story Instagram post causes duplicate stories to be published upon retry due to flawed deduplication logic.
Severity: HIGH
Suggested Fix
The publishing logic should be updated to handle retries of multi-part posts. Instead of re-publishing all items, the loop should check which individual stories have already been published and skip them. This could be achieved by tracking the published state of each media item separately, rather than relying on a single releaseId for the entire batch.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location:
libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts#L749-L771
Potential issue: When publishing multiple Instagram stories in a single action, the
system updates the post's `releaseId` in the database after each individual story is
successfully posted. If the process is interrupted or fails after some stories have been
published, a subsequent retry will cause duplicate posts. This happens because the
deduplication logic compares the `releaseId` from the database with the `releaseId` of
the post object, which are identical in this partial failure scenario. The check fails
to prevent a re-publish, and the entire sequence of stories is posted again from the
beginning.
Also affects:
apps/orchestrator/src/activities/post.activity.ts:229~242
Did we get this right? 👍 / 👎 to inform future reviews.
…ent duplicates and false failures When an Instagram post fails AFTER media_publish succeeded, the post is marked ERROR and the user emailed "we couldn't post" — even though the post is live — and nothing stops the publish from happening again: a Temporal retry of the activity (or the user, prompted by the false failure) publishes a duplicate. postSocial gave the platform no memory of a publish until the entire activity completed. The production occurrence is fully visible in Temporal: postSocial failed with Instagram's "Application request limit reached" (code 4, "blocked" subcode 2207051) after ~2 minutes — at or after the publish step — while the post went live. That error cannot be triggered on demand, so both failure boundaries were reproduced on unpatched code with synthetic failures and a real Instagram account: - a failing permalink fetch right after a successful publish left the post live on Instagram while Postiz marked it ERROR and emailed an error — the false-failure variant - a crash after a successful publish (simulating the activity dying or timing out) made the Temporal retry publish a second, identical Instagram post, with a green workflow and a success email — invisible duplicates Changes: - provider post(): report each publish to a new optional progress callback the moment Instagram confirms it, before any later step can fail - postSocial: persist the remote id via that callback (updatePost), and skip publishing on retry when the persisted releaseId differs from the one the workflow loaded before the activity ran — the difference can only come from an earlier attempt of this activity. Comparing against the workflow snapshot (instead of just checking existence) keeps repeatable posts working: they re-run with the same post id and an old releaseId already set - provider post(): the permalink fetch is now non-fatal — the post is already live when it runs, so a failure (rate limit included) falls back to the account URL instead of failing a successful publish - provider post(): label every Graph call with a step identifier (instagram-create-media, instagram-media-status, instagram-media-publish, instagram-create-carousel, instagram-permalink) so Temporal failures name the exact call that failed - linkedin providers: thread the new optional progress parameter through post() (they had a trailing options parameter in the same position) Validated on the patched code with the same two scenarios: - permalink failure: post published, no error email, post shown as published with the account URL as the release URL - crash after publish: exactly one Instagram post; the retried attempt hit the guard, skipped publishing and completed the workflow successfully Remaining gap (out of scope): when Instagram returns an error on the media_publish call itself but processes the publish anyway, the post is still marked failed — undetectable from the response. A possible future hardening is verifying against the account's recent media before failing the post. Known limitation: multi-media stories publish in a loop; the checkpoint is per story, so a retry after a mid-loop crash returns the already- published stories without duplicating them, but does not publish the remainder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a6a4e43 to
23aa46f
Compare
|
Contribution-checker quality warning Heuristics that flagged:
If this is a genuine contribution, please add detail to your PR description and tighten the diff scope before reviewers look at it. |
| integration, | ||
| async (response) => { | ||
| // Persist the remote id the moment the platform confirms the | ||
| // publish, so a crash or retry after this point cannot publish a | ||
| // duplicate. | ||
| await this._postService.updatePost( | ||
| response.id, | ||
| response.postId, | ||
| response.releaseURL | ||
| ); | ||
| } |
There was a problem hiding this comment.
Bug: The deduplication logic for LinkedIn posts is ineffective because the providers don't invoke the progress callback, which can lead to duplicate posts on retry.
Severity: MEDIUM
Suggested Fix
Modify the LinkedIn providers (linkedin.provider.ts and linkedin.page.provider.ts) to invoke the progress callback with the remote post ID after a successful API call. This will persist the ID, allowing the deduplication guard in postSocial to prevent duplicate posts during retries.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: apps/orchestrator/src/activities/post.activity.ts#L268-L281
Potential issue: The deduplication logic in the `postSocial` activity is ineffective for
LinkedIn posts. The guard at `post.activity.ts:230-242` depends on the `progress`
callback to persist the remote post ID. However, the LinkedIn providers
(`linkedin.provider.ts` and `linkedin.page.provider.ts`) accept this callback but never
invoke it. If a post to LinkedIn succeeds but the activity fails before returning, a
retry will bypass the deduplication check, resulting in a duplicate post being created.
User incident
A user reported that a scheduled Instagram post triggered the error email "An error occurred while posting on instagram: Instagram blocked your request" — yet the post was published on Instagram, and retrying created duplicate posts.
The Jul 5 occurrence is fully characterized from Temporal history:
postSocialfailed with Graph code 4 "Application request limit reached" / subcode 2207051 ("action is blocked") — Instagram's app-level rate limit. Our error mapping surfaces subcode 2207051 as "Instagram blocked your request" (deterministically — every 2207051 response produces that exact email).attempt: 1, no retries (2207051 is classifiedbad-body→ non-retryable at every layer), so that occurrence's duplicate risk came from the user reasonably retrying a post that was falsely marked failed.Root cause
postSocialpersists nothing until the entire activity completes. The Instagram flow is multi-step (create container → poll status →media_publish→ fetch permalink), so any failure aftermedia_publish:Reproduction (unpatched code, real Instagram account)
Fix
progresscallback the moment Instagram confirms it;postSocialpersists the remote id (updatePost) right there.releaseIddiffers from the snapshot the workflow loaded before the activity ran, an earlier attempt already published — return the persisted result instead of publishing again. Comparing against the snapshot (not mere existence) keeps repeatable posts working: they re-run with the same post id and an oldreleaseIdalready set.instagram-create-media,instagram-media-status,instagram-media-publish,instagram-create-carousel,instagram-permalink): every future failure names the exact call, so publish-step vs post-publish failures are distinguishable at a glance.progressparameter throughpost()(they had a trailing parameter in the same position).Post-fix validation (same scenarios, patched code)
attempt: 2visible in Temporal with the simulated failure aslastFailure), skipped publishing, and completed successfully.Remaining gap (out of scope for this PR)
If Instagram returns an error on the
media_publishcall itself but processes the publish anyway (which Meta occasionally does under rate limiting), the post is still marked failed — this is undetectable from the publish response. After this fix, that is the only case where a failure email can coexist with a live post; users seeing "Instagram blocked your request" during heavy posting should double-check Instagram before retrying.Possible follow-up to also remove the error in this edge case — verify-then-decide: when
media_publishfails with a rate-limit/block-type error, query the account's recent media (GET /{ig-user-id}/media?fields=id,caption,timestamp,permalink&limit=10) before failing the post:Blanket-treating publish-call rate limits as success is NOT an option: in the common case the post really wasn't published, and a false "published" (silent publish loss) is worse than a false "failed". The timestamp window must be tight so a repeatable post's previous cycle (identical caption) can't false-match, and stories are excluded initially (different edge, murkier matching).
Known limitation
Multi-media stories publish in a loop with a per-story checkpoint: a retry after a mid-loop crash returns the already-published stories without duplicating them, but does not publish the remainder — biased toward never duplicating.
🤖 Generated with Claude Code