{"record":{"id":"e3eaead7e11db4e0","repo":"gitroomhq/postiz-app","slug":"refresh-token-error-e3eaea","errorCode":null,"errorMessage":"Refresh token error","messagePattern":"Refresh token error","errorType":"exception","errorClass":"RefreshToken","httpStatus":401,"severity":"error","filePath":"libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts","lineNumber":319,"sourceCode":"          },\n          // @ts-ignore - undici-only option; blocks SSRF to internal IPs\n          dispatcher: getSsrfSafeDispatcher(),\n        } as any);\n      } catch (err) {\n        // Transient status-check error: the media may finish processing just\n        // fine, keep polling - if the instance stays broken the workflow\n        // exhausts its checks and warns the user properly.\n        return { status: 'pending', pendingData };\n      }\n\n      if (response.status === 206) {\n        // consume the body so the undici socket is released between polls\n        await response.text().catch(() => '');\n        return { status: 'pending', pendingData };\n      }\n\n      if (response.status === 401) {\n        throw new RefreshToken(\n          this.identifier,\n          await response.text().catch(() => '{}'),\n          '{}'\n        );\n      }\n\n      if (response.status === 404 || response.status === 410) {\n        // The media GET only sees UNATTACHED media: after a finalize whose\n        // outcome was lost, an already-published status makes its media 404\n        // here. Never conclude \"expired, post again\" (that instruction is the\n        // duplicate path) - keep checking the remaining media (a later 206\n        // still wins) and let the idempotent finalize either dedupe into the\n        // existing status or get Mastodon's verdict.\n        await response.text().catch(() => '');\n        continue;\n      }\n\n      if (response.status === 422) {","sourceCodeStart":301,"sourceCodeEnd":337,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts#L301-L337","documentation":"While polling a pending Mastodon post status, the Mastodon API answered 401, so the access token is no longer valid. Thrown as a RefreshToken error so the platform attempts token refresh/re-authentication. Note: Mastodon tokens are long-lived; a 401 usually means revoked or wrong-scoped token or app credentials changed.","triggerScenarios":"GET of the scheduled status returns HTTP 401 — token revoked by user, app deauthorized, or instance changed client credentials.","commonSituations":"User revoked the app from Mastodon settings, instance admin revoked the app, token stored against a different instance, or clock/env issues sending stale auth header.","solutions":["Reconnect the Mastodon integration to obtain a fresh access token","Verify the token and instance URL match (token from mastodon.social used against another instance)","Confirm the app still exists and has read:statuses/write:media scopes"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"function isRefreshTokenError(e: unknown): e is RefreshToken {\n  return e instanceof Error && (e as any).name === 'RefreshToken';\n}","tryCatchPattern":"catch (e) { if (isRefreshTokenError(e)) await refreshTokenAndRetry(); }","preventionTips":["Monitor 401 rates per integration","Prompt users to reconnect when Mastodon tokens age"],"tags":["mastodon","oauth","unauthorized"],"backgroundTag":"oauth-token-invalid","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}