hcengineering/platform · warning · ApiError

err.code

err.code

Error message

err.message (500 fallback sends err object when message is empty)

What it means

The ai-bot server's global Express error handler logs the error, returns ApiError instances with their own code and message, and for all other errors responds 500 with {message: err.message} — or the raw err object when message is empty. Seeing the raw error object in the response means a non-ApiError with an empty/absent message escaped a handler.

Source

Thrown at services/ai-bot/pod-ai-bot/src/server/server.ts:192

      const resp = await controller.getLoveIdentity(roomName)

      if (resp === undefined) {
        throw new ApiError(404)
      }

      res.status(200)
      res.json(resp)
    })
  )

  app.use((err: any, _req: any, res: any, _next: any) => {
    console.log(err)
    if (err instanceof ApiError) {
      res.status(err.code).send({ code: err.code, message: err.message })
      return
    }

    res.status(500).send(err.message?.length > 0 ? { message: err.message } : err)
  })

  return app
}

export function listen (e: Express, port: number, host?: string): Server {
  const cb = (): void => {
    console.log(`AI service has been started at ${host ?? '*'}:${port}`)
  }

  return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb)
}

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Read the raw error object in the 500 response (or console.log output) to identify the true failure.
  2. Wrap known failure paths in ApiError with a code and message so clients get structured errors.
  3. Ensure thrown values are Error instances with descriptive messages.
  4. Add middleware-level logging/serialization for non-Error throws.

Example fix

// before
throw { status: 502 }
// after
throw new ApiError(502, 'upstream service unavailable')
Defensive patterns

Strategy: try-catch

Validate before calling

// Client: detect the raw-object 500 shape before using it
function extractServerMessage(body: unknown): string {
  if (typeof body === 'object' && body !== null && 'message' in body && typeof (body as any).message === 'string') return (body as any).message
  return 'unstructured server error: ' + JSON.stringify(body)
}

Type guard

function isApiErrorPayload(x: unknown): x is { code: number; message: string } {
  return typeof x === 'object' && x !== null && typeof (x as any).code === 'number' && typeof (x as any).message === 'string'
}

Try / catch

try {
  return await callAiBot(req)
} catch (err) {
  if (err instanceof ApiError) {
    return res.status(err.code).json({ code: err.code, message: err.message })
  }
  logger.error({ err }, 'unhandled ai-bot error')
  return res.status(500).json({ message: err instanceof Error && err.message ? err.message : 'internal error' })
}

Prevention

When it happens

Trigger: Any unhandled exception thrown in a route that is not an ApiError and whose err.message is empty or undefined — e.g. Error(''), non-Error objects thrown, or fetch/network errors without messages.

Common situations: Upstream HTTP client rejections yielding message-less errors; throwing plain objects or strings; bugs producing empty Error instances; unhandled promise rejections reaching the error middleware.

Related errors


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