yikart/AiToEarn · error

Session not found

Error message

Session not found

What it means

The MCP SSE service routes POST messages to a per-session transport held in an in-memory map. If the sessionId query parameter has no registered transport, the request is rejected with 404 'Session not found' — the client is addressing a session the server no longer knows.

Source

Thrown at project/aitoearn-backend/libs/nest-mcp/src/services/mcp-sse.service.ts:110

      this.mcpServers.delete(sessionId)
      this.pingService.removeConnection(sessionId)
    }

    await mcpServer.connect(transport)
  }

  /**
   * Handle message processing for SSE
   */
  async handleMessage(rawReq: any, rawRes: any, body: unknown): Promise<any> {
    const adapter = HttpAdapterFactory.getAdapter(rawReq, rawRes)
    const req = adapter.adaptRequest(rawReq)
    const res = adapter.adaptResponse(rawRes)
    const sessionId = req.query['sessionId'] as string
    const transport = this.transports.get(sessionId)

    if (!transport) {
      return res.status(404).send('Session not found')
    }

    const mcpServer = this.mcpServers.get(sessionId)
    if (!mcpServer) {
      return res.status(404).send('MCP server not found for session')
    }

    // Resolve the request-scoped tool executor service
    const contextId = ContextIdFactory.getByRequest(req)
    const executor = await this.moduleRef.resolve(
      McpExecutorService,
      contextId,
    )

    // Register request handlers with the user context from this specific request
    executor.registerRequestHandlers(mcpServer, req)

    // Process the message

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Re-establish the SSE connection (GET /sse) to obtain a fresh sessionId, then use it for POSTs
  2. Ensure sticky sessions / same-instance routing so POSTs reach the instance holding the transport
  3. Implement client logic to detect 404 and automatically reconnect the SSE stream
  4. Enable SSE heartbeat/keepalive and tune timeouts so idle sessions are not dropped

Example fix

// before
await post(endpoint, { ...body, sessionId: storedSessionId }) // stale after restart
// after
try {
  await post(endpoint, body, { params: { sessionId } })
} catch (e) {
  if (isSessionNotFound(e)) { sessionId = await reconnectSse() }
}
Defensive patterns

Strategy: retry

Validate before calling

// before POSTing, confirm the SSE stream is open and the id is current
if (!sseConnectionOpen || Date.now() - sessionIssuedAt > MAX_SESSION_TTL) {
  sessionId = await reconnectSse()
}

Type guard

function isSessionNotFound(resp: { status: number; body?: unknown }): boolean {
  return resp.status === 404 && typeof resp.body === 'string' && resp.body.includes('Session not found')
}

Try / catch

try {
  await postMessage(sessionId, payload)
} catch (e) {
  if (isSessionNotFoundErr(e)) {
    sessionId = await reconnectSse() // GET /sse for a new id
    await postMessage(sessionId, payload)
  } else { throw e }
}

Prevention

When it happens

Trigger: POST to the SSE message endpoint with a sessionId that was never opened, or whose SSE connection was closed/server restarted, so transports no longer contains the id.

Common situations: Server restart or multi-instance deployment where the SSE connection lives on another pod; client reconnecting POSTs with a stale sessionId after network drop; load balancer routing POSTs to a different instance than the GET /sse; long idle timeout closing the SSE stream while the client keeps POSTing.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/dedce6c0a4cd92e4. Report an issue: GitHub.