fix(integrations): don't fail channel creation when the avatar upload fails#1762
Merged
Merged
Conversation
… 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>
✅ 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/facebookwith the exceptionError: 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:
authenticate()returns the user's avatar URL fromme?fields=id,name,picture.IntegrationService.createOrUpdateIntegrationre-hosts that avatar onto our own storage before writing anything to the database, because provider CDN URLs are signed and expire.uploadSimplefetches the URL, buffers whatever comes back without checkingresponse.ok, and runsfile-typeover it. If the CDN answered with an error page, an empty body, or anything that isn't a recognizable image, it throwsnew Error('Unsupported file type.').Error, not anHttpException, so Nest returns500 {"statusCode":500,"message":"Internal server error"}and the callback screen renders exactly the message the customer quoted._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:
createand theupdatebranch),Integration.pictureis nullable, the API returnsp.picture || '/no-picture.jpg', and roughly twenty frontend call sites do the same fallback (several with an additionalfallbackSrc/onErrorswap).social-connectis the user's personal profile photo;saveProviderPagethen 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./no-picture.jpguntil it is reconnected: the periodic token refresh will not pick the avatar up later, becauseRefreshIntegrationService.refreshpasses the already-stored picture rather than re-fetching one from the provider.This also protects token refresh.
RefreshIntegrationService.refreshfeeds the stored picture back intocreateOrUpdateIntegration, and since that URL is our own bucket rather thanimagedelivery.net, every refresh re-downloads and re-uploads our own copy. Before this change, a failure there would throw inside therefreshTokenactivity 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 withenableLogsplus 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:
uploadSimplenot checkingresponse.ok, which is why the message is "Unsupported file type" rather than something about an HTTP error.IntegrationRepository.updateIntegrationfor the page-selection step. It can fail identically, but there are no Sentry events for that endpoint, so it is out of scope here.||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: