Skip to content

Memory leak: child span not ended when Root HTTP request is aborted #62

@gazanna

Description

@gazanna

What version of Elysia is running?

1.4.17

What platform is your computer?

Darwin 25.1.0 arm64 arm

What steps can reproduce the bug?

import { Elysia } from 'elysia'
import { opentelemetry } from '@elysiajs/opentelemetry'
import { trace } from '@opentelemetry/api'

const spans = new Map<string, { name: string, ended: boolean }>()

function instrumentSpans() {
  const originalGetTracer = trace.getTracer.bind(trace)

  trace.getTracer = function (...args: any[]) {
    const tracer = originalGetTracer(...args)
    const originalStartSpan = tracer.startSpan.bind(tracer)

    tracer.startSpan = function (name: string, options?: any, context?: any) {
      const span = originalStartSpan(name, options, context)
      const id = span.spanContext().spanId

      spans.set(id, { name, ended: false })

      const originalEnd = span.end.bind(span)
      span.end = function (...args: any[]) {
        const record = spans.get(id)
        if (record) record.ended = true
        return originalEnd(...args)
      }

      return span
    }

    return tracer
  }
}

async function test() {
  spans.clear()
  instrumentSpans()

  const app = new Elysia()
    .use(opentelemetry())
    .get('/slow', async () => {
      await new Promise(r => setTimeout(r, 10000))
      return 'done'
    })
    .listen(3400)

  const controller = new AbortController()
  const request = fetch('http://localhost:3400/slow', {
    signal: controller.signal
  }).catch(() => { })

  await new Promise(r => setTimeout(r, 100))
  controller.abort()
  await request
  await new Promise(r => setTimeout(r, 5000))

  const leaked = Array.from(spans.values()).filter(s => !s.ended)

  console.log(`Total spans: ${spans.size}`)
  console.log(`Leaked spans: ${leaked.length}`)

  if (leaked.length > 0) {
    console.log('\nLeaked:')
    leaked.forEach(s => { console.log(`  - ${s.name}`) })
  }

  app.stop()
  process.exit(leaked.length > 0 ? 1 : 0)
}

test()

What is the expected behavior?

to end child spans

What do you see instead?

No response

Additional information

When a client aborts an HTTP request (e.g., via AbortController), the OpenTelemetry Handle span is never ended, causing a memory leak.
child spans should end to avoid a memory leak

Have you try removing the node_modules and bun.lockb and try again yet?

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions