From 136530eeba9c4336d8bc5648aefea9c48e7fd72d Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:03:53 -0700 Subject: [PATCH] fix(pg): the readiness mount check sees a router Sentry has wrapped Deploy 4e60f240 (#1901) hung at the backend rollout. The new pod connected to Postgres and mounted /api/pg/messages (it answered 401), but /api/health/ready said "PostgreSQL routes are not mounted on this pod" and the pod never became Ready. routerIsMounted compared layer.handle === router, and @sentry/node 10.65, initialised in production, wraps every Express layer handle. Reproduced locally with the backend's own modules: with Sentry initialised the identity check is false and the router's stack is still the same array; without Sentry both are true. The check now also accepts a layer whose handle exposes the router's own stack array. That is still an identity question about this router, not a path match. Two tests build a real Express app and wrap the layer the way Sentry does: the mounted router is found, and a different wrapped router is not. The first is red on main's pgBootService. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RUTWqLu6zhFPraoPHZcjQP --- .../unit/services/pgBootService.test.js | 36 +++++++++++++++++++ backend/services/pgBootService.ts | 11 ++++++ 2 files changed, 47 insertions(+) diff --git a/backend/__tests__/unit/services/pgBootService.test.js b/backend/__tests__/unit/services/pgBootService.test.js index 8fe138bee..ceaa033a5 100644 --- a/backend/__tests__/unit/services/pgBootService.test.js +++ b/backend/__tests__/unit/services/pgBootService.test.js @@ -203,6 +203,42 @@ describe('pgBootService — the route-table probe (TASK-168)', () => { expect(routerIsMounted(appWith(layer(pgMessages)), pgMessages)).toBe(true); }); + // Production runs @sentry/node, which wraps every Express layer handle: the + // wrapper is a new function that exposes the original's properties. Identity + // on the handle then fails on a pod that has the route mounted (deploy + // 4e60f240, 2026-09-27). This builds a real Express app and wraps the layer + // the same way. + const wrapLikeSentry = (app, router) => { + const target = app._router.stack.find((l) => l.handle === router); + const original = target.handle; + const wrapper = function wrapped(...args) { return original.apply(this, args); }; + Object.defineProperty(wrapper, 'stack', { get: () => original.stack }); + target.handle = wrapper; + return target; + }; + + it('finds the mounted router when instrumentation has wrapped its layer handle', () => { + const express = require('express'); + const app = express(); + const pgMessages = express.Router(); + pgMessages.get('/:id', (req, res) => res.sendStatus(401)); + app.use('/api/pg/messages', pgMessages); + const target = wrapLikeSentry(app, pgMessages); + expect(target.handle).not.toBe(pgMessages); + expect(routerIsMounted(app, pgMessages)).toBe(true); + }); + + it('does not mistake a different wrapped router for this one', () => { + const express = require('express'); + const app = express(); + const other = express.Router(); + other.get('/', (req, res) => res.sendStatus(200)); + app.use('/api/pg/status', other); + wrapLikeSentry(app, other); + const pgMessages = express.Router(); + expect(routerIsMounted(app, pgMessages)).toBe(false); + }); + it('returns false when that router is absent, which is the incident', () => { const pgMessages = { name: 'pg-messages' }; const pgStatus = { name: 'pg-status' }; diff --git a/backend/services/pgBootService.ts b/backend/services/pgBootService.ts index 23b1c4be3..85d7afc8d 100644 --- a/backend/services/pgBootService.ts +++ b/backend/services/pgBootService.ts @@ -125,13 +125,24 @@ export const PG_MESSAGE_PATH = '/api/pg/messages'; * IS the thing being mounted, so asking whether that object is in the stack is * the same question with no translation step — and it stays correct if the path * ever changes. + * + * The router's own `stack` array is the identity that survives instrumentation. + * In production `@sentry/node` (10.65) wraps every Express layer handle, so + * `layer.handle === router` is false on a pod that has the route mounted. The + * wrapper exposes the original's properties, so `layer.handle.stack` is still + * the router's stack. 2026-09-27: deploy 4e60f240 hung, because the new pod + * answered /api/pg/messages with 401 while /api/health/ready said "not mounted". + * A local reproduction with Sentry initialised gives identity false and stack + * true; without Sentry, both are true. */ export const routerIsMounted = (app: unknown, router: unknown): boolean => { if (!router) return false; + const routerStack = (router as { stack?: unknown }).stack; const walk = (candidate: any): boolean => { const stack = (candidate && candidate.stack) || []; return stack.some((layer: any) => { if (layer.handle === router) return true; + if (routerStack && layer.handle && layer.handle.stack === routerStack) return true; return Boolean(layer.handle && layer.handle.stack) && walk(layer.handle); }); };