Skip to content

fix(instrumentation-router): prevent MaxListenersExceededWarning on response stream - #3637

Open
bhuvan-somisetty wants to merge 1 commit into
open-telemetry:mainfrom
bhuvan-somisetty:fix/router-stream-max-listeners-3547
Open

fix(instrumentation-router): prevent MaxListenersExceededWarning on response stream#3637
bhuvan-somisetty wants to merge 1 commit into
open-telemetry:mainfrom
bhuvan-somisetty:fix/router-stream-max-listeners-3547

Conversation

@bhuvan-somisetty

@bhuvan-somisetty bhuvan-somisetty commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Which problem is this PR solving?

Fixes #3547

When using streams or async handlers with @opentelemetry/instrumentation-router and @opentelemetry/instrumentation-express, each pending layer registers a close listener on the response object via ensureFallbackListener(). When multiple async handlers or stream events stack up, Node emits a MaxListenersExceededWarning: Possible EventEmitter memory leak detected because res.getMaxListeners() is exceeded.

Short description of the changes

  • Increment res.getMaxListeners() by 1 whenever attaching a fallback close listener in both @opentelemetry/instrumentation-router and @opentelemetry/instrumentation-express.
  • Decrement res.getMaxListeners() back down when the close event fires or when removeListener('close', ...) is called via wrappedNext / callback completion.
  • Added unit tests in instrumentation-router verifying that async middleware with streaming responses and pre-populated listeners do not trigger MaxListenersExceededWarning.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Added unit tests in packages/instrumentation-router/test/index.test.ts covering async middleware and response streaming scenarios to assert no MaxListenersExceededWarning is emitted.

Checklist

  • Followed the style guidelines of this project
  • Unit tests added/updated for the change
  • No new warnings introduced

@bhuvan-somisetty
bhuvan-somisetty requested a review from a team as a code owner July 23, 2026 15:29
@github-actions github-actions Bot added pkg:instrumentation-express pkg:instrumentation-router pkg-status:unmaintained This package is unmaintained. Only bugfixes may be acceped until a new owner has been found. labels Jul 23, 2026
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jul 23, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-08-10 13:49 UTC

Review the latest changes.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@github-actions

Copy link
Copy Markdown
Contributor

This package does not have an assigned component owner and is considered unmaintained. As such this package is in feature-freeze and this PR will be closed with 14 days unless a new owner or a sponsor (a member of @open-telemetry/javascript-approvers) for the feature is found. It is the responsibility of the author to find a sponsor for this feature.
Are you familiar with this package? Consider becoming a component owner.

Comment thread scripts/bitrot.mjs
Comment on lines +30 to 181
// `glob` always wants forward slashes in its patterns, even on Windows,
// where `path.join` produces backslashes.
function toGlobPattern(...parts) {
return path
.join(...parts)
.split(path.sep)
.join('/');
}

let numProbs = 0;
function problem(...args) {
numProbs += 1;
if (USE_COLOR) {
process.stdout.write('\x1b[31m');
}
args.unshift('bitrot error:');
console.log(...args);
if (USE_COLOR) {
process.stdout.write('\x1b[39m');
}
}

function warn(...args) {
if (USE_COLOR) {
process.stdout.write('\x1b[33m');
}
args.unshift('bitrot warn:');
console.warn(...args);
if (USE_COLOR) {
process.stdout.write('\x1b[39m');
}
}

function gitCloneSync(repo, dir) {
execSync(`git clone ${repo} "${dir}"`);
}
function gitPullSync(cwd) {
execSync('git pull', { cwd });
}

function isPublicPackage(pj) {
if (pj.private === true) {
return false;
} else if (pj.publishConfig?.access) {
return pj.publishConfig.access === 'public';
} else {
// Default is *false* for scoped packages.
return !pj.name.startsWith('@');
}
}

/**
* BITROT: Check that the `"groupName": "OTel Core experimental",`
* group in renovate.json matches the actual current set of experimental
* packages from the core repo.
*/
function bitrotRenovateCoreExperimental() {
const renovateJson = path.join(TOP, 'renovate.json');
const renovate = JSON.parse(fs.readFileSync(renovateJson));
const group = renovate.packageRules.filter(
r => r.groupName === 'OTel Core experimental'
)[0];
assert.ok(group, `found "OTel Core experimental" group in ${renovateJson}`);

const ojDir = path.join(BUILD_DIR, 'opentelemetry-js');
if (fs.existsSync(ojDir)) {
gitPullSync(ojDir);
} else {
gitCloneSync(
'https://github.com/open-telemetry/opentelemetry-js.git',
ojDir
);
}

const pkgNames = globSync(
path.join(ojDir, 'experimental/packages/*/package.json')
toGlobPattern(ojDir, 'experimental/packages/*/package.json')
)
.map(packageJson => JSON.parse(fs.readFileSync(packageJson)))
.filter(pj => isPublicPackage(pj))
.map(pj => pj.name);

const pkgNamesSet = new Set(pkgNames);
const renovateSet = new Set(group.matchPackageNames);
const missing = pkgNamesSet.difference(renovateSet); // requires Node.js >=22
const extraneous = renovateSet.difference(pkgNamesSet);

const issues = [];
if (missing.size) {
issues.push(`missing entries: ${JSON.stringify(Array.from(missing))}`);
}
if (extraneous.size) {
issues.push(
`extraneous entries: ${JSON.stringify(Array.from(extraneous))}`
);
}
if (issues.length) {
problem(
`${renovateJson}: "matchPackageNames" in the "OTel Core experimental" group does not match the current set experimental packages from the opentelemetry-js.git repo:\n - ${issues.join(
'\n - '
)}\nThe "matchPackageNames" should be:\n${JSON.stringify(
pkgNames.sort(),
null,
2
)}`
);
}
}

function getNpmInfo(name) {
const CACHE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
const cache = ensureCacheLoaded('npmInfo');
const cacheEntry = cache[name];
if (cacheEntry) {
if (cacheEntry.timestamp + CACHE_TIMEOUT_MS > Date.now()) {
return cacheEntry.value;
} else {
delete cache[name];
}
}

// Limited security guard on exec'ing given `name`.
const PKG_NAME_RE = /^(@[\w_.-]+\/)?([\w_.-]+)$/;
if (!PKG_NAME_RE.test(name)) {
throw new Error(
`${JSON.stringify(name)} does not look like a valid npm package name`
);
}

const stdout = execSync(`npm info --json "${name}"`);
const npmInfo = JSON.parse(stdout);

cache[name] = {
timestamp: Date.now(),
value: npmInfo,
};
saveCache();
return npmInfo;
}

/**
* BITROT: Check if instrumentations are missing support for major releases of
* the target package.
*
* Limitations:
* - This doesn't currently handle both `pg` and `pg-pool` from instr-pg.
* - This doesn't currently support instr-aws-sdk, because of the wildcard in
* the packages supported: `@aws-sdk/client-*`.
*/
function bitrotInstrumentations() {
const instrReadmes = globSync(
path.join(TOP, 'packages/instrumentation-*/README.md')
toGlobPattern(TOP, 'packages/instrumentation-*/README.md')
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove these unrelated changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @pichlermarc, thanks for catching that! I've rebased the branch on latest main and removed the unrelated changes.

@bhuvan-somisetty
bhuvan-somisetty force-pushed the fix/router-stream-max-listeners-3547 branch from 011decc to 5a57b29 Compare July 31, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:instrumentation-express pkg:instrumentation-router pkg-status:unmaintained:autoclose-scheduled pkg-status:unmaintained This package is unmaintained. Only bugfixes may be acceped until a new owner has been found.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Listener memory leak warning triggered in Node express 5 app using streams

5 participants