hcengineering/platform · warning · ApiError
User already authorized
Error message
User already authorized
What it means
ApiError(409, 'User already authorized') is thrown by the telegram-bot token-exchange endpoint when getAnyIntegrationByAccount(token.account) returns an existing integration. It means this account has already completed the Telegram authorization flow, so creating a second integration for the same account would duplicate it; the server rejects the request with HTTP 409 Conflict.
Source
Thrown at services/telegram-bot/pod-telegram-bot/src/server.ts:114
)
app.post(
'/auth',
wrapRequest(async (req, res, token) => {
if (req.body == null || typeof req.body !== 'object') {
throw new ApiError(400)
}
const { code } = req.body
if (code == null || code === '' || typeof code !== 'string') {
throw new ApiError(400)
}
const integration = await getAnyIntegrationByAccount(token.account)
if (integration !== undefined) {
throw new ApiError(409, 'User already authorized')
}
const person = await getAccountPerson(token.account)
if (person === undefined) {
throw new ApiError(404, 'Person not found')
}
const newRecord = await worker.authorizeUser(code, token.account, token.workspace)
if (newRecord === undefined) {
throw new ApiError(500)
}
void worker.limiter.add(newRecord.telegramId, async () => {
ctx.info('Connected account', { account: token.account, username: newRecord.username })
const message = await translate(telegram.string.AccountConnectedHtml, {
app: config.App,
name: `${person.firstName} ${person.lastName}`
})View on GitHub (pinned to 63e28dc964)
Solutions
- Check the account's existing integration first and treat 409 as success / show 'already connected' in the UI instead of an error.
- Before re-authorizing, remove or disconnect the existing integration for the account, then retry the flow.
- Guard the client flow with a state flag so the exchange request fires only once per authorization attempt.
- Make the endpoint idempotent: if an integration already exists for the account, return 200 with the existing record instead of throwing.
Example fix
// before
if (integration !== undefined) {
throw new ApiError(409, 'User already authorized')
}
// after
if (integration !== undefined) {
return { status: 200, result: integration } // idempotent: already connected
} Defensive patterns
Strategy: try-catch
Validate before calling
const existing = await getAnyIntegrationByAccount(account)
if (existing !== undefined) {
return existing // already authorized; skip the exchange call entirely
} Type guard
function isAlreadyAuthorized(err: unknown): err is ApiError {
return err instanceof ApiError && (err as ApiError).message === 'User already authorized'
} Try / catch
try {
await exchangeToken(token, code)
} catch (err) {
if (err instanceof ApiError && err.message === 'User already authorized') {
showAlreadyConnected(); return
}
throw err
} Prevention
- Check for an existing integration before starting the authorization flow.
- Disable the connect button once the account is linked.
- Treat 409 from the endpoint as a success condition in retry logic.
When it happens
Trigger: A user who previously linked their Telegram account re-submits an OAuth code to the exchange endpoint; getAnyIntegrationByAccount finds an integration for token.account and the handler throws before calling worker.authorizeUser.
Common situations: Double-clicking the 'Connect Telegram' button, retrying a flow that actually succeeded on the first attempt (client never saw the success response), or re-running an onboarding script against an already-connected account.
Related errors
- platform.status.SocialIdAlreadyExists
- AccountAlreadyExists
- Conflict
- Both SMTP and SES configuration are specified, please specif
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/de079737f8420a65.
Report an issue: GitHub.