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

  1. Check the account's existing integration first and treat 409 as success / show 'already connected' in the UI instead of an error.
  2. Before re-authorizing, remove or disconnect the existing integration for the account, then retry the flow.
  3. Guard the client flow with a state flag so the exchange request fires only once per authorization attempt.
  4. 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

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


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/de079737f8420a65. Report an issue: GitHub.