Skip to content

fix(integrations): don't fail channel creation when the avatar upload fails#1762

Merged
nevo-david merged 1 commit into
mainfrom
fix/avatar-upload-should-not-block-channel
Jul 23, 2026
Merged

fix(integrations): don't fail channel creation when the avatar upload fails#1762
nevo-david merged 1 commit into
mainfrom
fix/avatar-upload-should-not-block-channel

Conversation

@giladresisi

@giladresisi giladresisi commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What kind of change does this PR introduce?

Bug fix.

Why was this change needed?

A customer reported being unable to add a Facebook Page channel, seeing only "Could not add provider / Internal server error". Sentry shows the matching backend errors on POST /integrations/social-connect/facebook with the exception Error: Unsupported file type. — 16 of them in a burst starting ~20:20 (UTC+7), which lines up exactly with when the ticket was opened. This error is otherwise very rare in our Sentry, so the sudden burst and the ticket are almost certainly the same event, most likely a spell of the Facebook CDN refusing our requests.

The path that produces it:

  1. OAuth succeeds — token is valid, scopes are granted. authenticate() returns the user's avatar URL from me?fields=id,name,picture.
  2. IntegrationService.createOrUpdateIntegration re-hosts that avatar onto our own storage before writing anything to the database, because provider CDN URLs are signed and expire.
  3. uploadSimple fetches the URL, buffers whatever comes back without checking response.ok, and runs file-type over it. If the CDN answered with an error page, an empty body, or anything that isn't a recognizable image, it throws new Error('Unsupported file type.').
  4. That is a plain Error, not an HttpException, so Nest returns 500 {"statusCode":500,"message":"Internal server error"} and the callback screen renders exactly the message the customer quoted.
  5. The throw happens before _integrationRepository.createOrUpdateIntegration, so nothing is persisted: no channel, no error state anywhere in the product, nothing for support to look at.

A cosmetic avatar should never be able to block channel creation, and it especially shouldn't here:

  • A missing picture is already a first-class state. The upsert omits the column when the value is undefined (both the create and the update branch), Integration.picture is nullable, the API returns p.picture || '/no-picture.jpg', and roughly twenty frontend call sites do the same fallback (several with an additional fallbackSrc/onError swap).
  • For a two-step provider like Facebook Page the avatar is discarded seconds later anyway. The picture uploaded during social-connect is the user's personal profile photo; saveProviderPage then overwrites name, picture and internalId with the page's values. So the upload that took the whole flow down was for an image that was about to be replaced, and the user still ends up with the correct thumbnail.
  • A failed fetch can never blank out an avatar that is already good. When the value is missing the update branch of the upsert omits the column, leaving whatever is stored untouched. Note this also means the channel keeps /no-picture.jpg until it is reconnected: the periodic token refresh will not pick the avatar up later, because RefreshIntegrationService.refresh passes the already-stored picture rather than re-fetching one from the provider.

This also protects token refresh. RefreshIntegrationService.refresh feeds the stored picture back into createOrUpdateIntegration, and since that URL is our own bucket rather than imagedelivery.net, every refresh re-downloads and re-uploads our own copy. Before this change, a failure there would throw inside the refreshToken activity and take the refresh workflow down with it.

Other information:

The failure is logged rather than swallowed silently, including the URL that failed — the current exception carries no information about the bytes we received, so it has no diagnostic value beyond the stack trace, and the log line is strictly more useful for chasing the next burst. This follows the pattern already used in this flow (console.log('Failed to fetch pages:', err), startRefreshWorkflow(...).catch((err) => console.log(err))), and with enableLogs plus the console integration these land in Sentry Logs.

The trade-off is that these stop being Sentry issues, so a future burst won't group or alert the way this one did — searchable as "Failed to upload profile picture" in Logs instead.

Deliberately left out of this PR:

  • uploadSimple not checking response.ok, which is why the message is "Unsupported file type" rather than something about an HTTP error.
  • The same re-host running in IntegrationRepository.updateIntegration for the page-selection step. It can fail identically, but there are no Sentry events for that endpoint, so it is out of scope here.
  • The || in that repository condition looks like it should be &&, so already-hosted pictures are re-uploaded on every update, orphaning the previous object in the bucket. Combined with the refresh behaviour above, every token refresh leaves another orphaned copy of the same image.

Checklist:

  • I have read the CONTRIBUTING guide.
  • I have signed the Contributor License Agreement (CLA).
  • I confirm I have not used AI to submit this PR or generate code for it.
  • I checked that there were no similar issues or PRs already open for this.
  • This PR fixes just ONE issue

… fails

A provider avatar that can't be re-hosted currently aborts the whole connect
flow: uploadSimple throws a plain Error('Unsupported file type.') when the
fetched bytes aren't a recognizable image, Nest turns that into a 500, and the
callback screen renders it as "Could not add provider / Internal server error".
The throw happens before the repository write, so nothing is persisted and the
user is left with no channel and no explanation.

The picture is cosmetic and a missing one is already a first-class state: the
upsert omits the column when it is undefined, Integration.picture is nullable,
and both the API and the frontend fall back to /no-picture.jpg. Swallow the
upload failure and log the URL that failed instead, so the channel is created
and the next re-auth picks the avatar up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@postiz-contribution postiz-contribution Bot added the contribution:approved Approved contributor label Jul 22, 2026
@postiz-agent

postiz-agent Bot commented Jul 22, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@nevo-david
nevo-david merged commit 7ba8924 into main Jul 23, 2026
11 checks passed
@nevo-david
nevo-david deleted the fix/avatar-upload-should-not-block-channel branch July 23, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution:approved Approved contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants