{"record":{"id":"607b8560f752091b","repo":"gitroomhq/postiz-app","slug":"channel-disconnected-due-to-expired-token","errorCode":null,"errorMessage":"Channel disconnected due to expired token","messagePattern":"Channel disconnected due to expired token","errorType":"http","errorClass":"HttpException","httpStatus":401,"severity":"critical","filePath":"apps/backend/src/public-api/routes/v1/public.integrations.controller.ts","lineNumber":613,"sourceCode":"          getIntegration.token,\n          body.data || {},\n          getIntegration.internalId,\n          getIntegration\n        );\n\n        return { output: result };\n      } catch (err) {\n        if (err instanceof RefreshToken) {\n          const data = await this._refreshIntegrationService.refresh(\n            getIntegration\n          );\n\n          if (!data) {\n            await this._integrationService.disconnectChannel(\n              org.id,\n              getIntegration\n            );\n            throw new HttpException(\n              { msg: 'Channel disconnected due to expired token' },\n              401\n            );\n          }\n\n          const { accessToken } = data;\n\n          if (accessToken) {\n            getIntegration.token = accessToken;\n\n            if (integrationProvider.refreshWait) {\n              await timer(10000);\n            }\n\n            continue;\n          }\n        }\n        throw new HttpException({ msg: 'Unexpected error' }, 500);","sourceCodeStart":595,"sourceCodeEnd":631,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/apps/backend/src/public-api/routes/v1/public.integrations.controller.ts#L595-L631","documentation":"After invoking a provider tool, the controller attempts to refresh the channel's OAuth credentials; if the refresh yields no data (refresh token revoked, expired, or the app was disconnected upstream), the integration is automatically disconnected via disconnectChannel and a 401 'Channel disconnected due to expired token' is thrown. This is a destructive side effect: the integration is removed as part of handling the failure.","triggerScenarios":"POST to triggerIntegrationTool on a channel whose OAuth access/refresh token is expired and cannot be refreshed (password changed on the platform, app authorization revoked, refresh token invalid), causing the provider refresh to return null.","commonSituations":"Long-lived integrations whose tokens lapse without a valid refresh token; user revoked the app in the channel's security settings; platform policy change invalidated refresh tokens (e.g. Instagram/Facebook re-auth requirements); sandbox app credentials rotated.","solutions":["Reconnect the channel in Postiz (the disconnect already happened server-side) to establish fresh tokens","Check the provider's developer portal for revoked permissions or expired app secrets","Verify the OAuth client secret/id env vars for that provider are still valid","Before calling tools, check the integration still exists (it won't after this fires) and alert owners on 401 rather than retrying"],"exampleFix":"// before\ntry {\n  await triggerTool(integration.id, 'sendPost', {...});\n} catch (e) { /* retries forever against a dead channel */ }\n\n// after\ntry {\n  await triggerTool(integration.id, 'sendPost', {...});\n} catch (e) {\n  if (e.status === 401) {\n    // channel was auto-disconnected: notify owner to reconnect, do NOT retry\n    await notifyOwner('Channel disconnected, please reconnect');\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const integration = (await listIntegrations()).find((i) => i.id === id);\nif (!integration) throw new Error('Channel already disconnected — reconnect required');","typeGuard":"const isTokenRefreshFailure = (e: unknown): boolean =>\n  (e as any)?.status === 401 && /expired token/i.test(JSON.stringify((e as any)?.response ?? (e as any)?.body ?? ''));","tryCatchPattern":"try {\n  await triggerTool(id, method, args);\n} catch (e: any) {\n  if (e?.status === 401) {\n    // server already disconnected the channel: alert owner, stop retrying\n    await notifyOwnerReconnectRequired(id);\n    return;\n  }\n  throw e;\n}","preventionTips":["Treat 401 from tool endpoints as terminal, never retry it","Monitor provider developer-portals for revoked app permissions and secret expiry","Surface reconnection prompts to channel owners before tokens lapse (e.g. long-lived Instagram/Facebook tokens)"],"tags":["oauth","token-expired","auto-disconnect","http-401","public-api"],"backgroundTag":"oauth-token-refresh-failed","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}