11'use strict'
22
33const METHODS = [ ...require ( 'http' ) . METHODS . map ( v => v . toLowerCase ( ) ) , 'all' ]
4+ const ROUTE_RESOLUTION_FAILED = Symbol ( 'routeResolutionFailed' )
45const shimmer = require ( '../../datadog-shimmer' )
56const { addHook, channel, createErrorPublisher } = require ( './helpers/instrument' )
67const { getCompileToRegexp } = require ( './path-to-regexp' )
@@ -35,6 +36,7 @@ function isFastSlash (layer, matchers) {
3536 * @param {{ handle: Function, name?: string, path?: string,
3637 * regexp?: { fast_star?: boolean, fast_slash?: boolean } }} layer
3738 * @param {Array<{ path?: string, regex?: RegExp }> & { hasStarPath?: boolean, hasSlashPath?: boolean } } matchers
39+ * @returns {void }
3840 */
3941function annotateLayer ( layer , matchers ) {
4042 const handle = layer . handle
@@ -71,6 +73,25 @@ function resolveLayerRoute (meta, layer) {
7173 }
7274}
7375
76+ /**
77+ * Preserve the host Layer's error boundary while resolving a route outside its
78+ * dispatch method. This path runs only for multi-pattern layers.
79+ *
80+ * @param {{ captureRoute?: string, needMultiMatch: boolean,
81+ * matchers: Array<{ path?: string, regex?: RegExp }> }} meta
82+ * @param {{ path?: string } } layer
83+ * @param {Function } next
84+ * @returns {string | undefined | typeof ROUTE_RESOLUTION_FAILED }
85+ */
86+ function resolveLayerRouteOrForwardError ( meta , layer , next ) {
87+ try {
88+ return resolveLayerRoute ( meta , layer )
89+ } catch ( error ) {
90+ next ( error )
91+ return ROUTE_RESOLUTION_FAILED
92+ }
93+ }
94+
7495/**
7596 * Build the request/error dispatch wrappers for one host (`express` / `router`).
7697 * They wrap the layer's prototype dispatch and read the side-table metadata, so
@@ -79,6 +100,11 @@ function resolveLayerRoute (meta, layer) {
79100 * span is published only for the layer the host actually runs.
80101 *
81102 * @param {string } name Channel namespace (`apm:<name>:middleware:*`).
103+ * @returns {{
104+ * wrapLayerRequest: (originalRequest: Function) => Function,
105+ * wrapLayerError: (originalError: Function) => Function,
106+ * wrapLegacyHandle: (layer: object, original: Function, guardRepeatedNext?: boolean) => Function
107+ * }}
82108 */
83109function createLayerDispatchWrappers ( name ) {
84110 const enterChannel = channel ( `apm:${ name } :middleware:enter` )
@@ -128,15 +154,25 @@ function createLayerDispatchWrappers (name) {
128154 // `wrappedNext` through captures both without a tracer-side try/catch; only
129155 // `exit` needs the `finally`. express 4's native dispatch converts only the
130156 // synchronous throw — exactly what the pre-refactor handle wrap caught.
157+ /**
158+ * @param {Function } originalRequest
159+ * @returns {Function }
160+ */
131161 function wrapLayerRequest ( originalRequest ) {
132162 return function ( req , res , next ) {
133163 if ( ! enterChannel . hasSubscribers ) return originalRequest . call ( this , req , res , next )
134164
135165 const meta = getLayerMeta ( this )
136166 if ( meta === undefined || this . handle . length > 3 ) return originalRequest . call ( this , req , res , next )
137167
168+ let route = meta . captureRoute
169+ if ( meta . needMultiMatch ) {
170+ route = resolveLayerRouteOrForwardError ( meta , this , next )
171+ if ( route === ROUTE_RESOLUTION_FAILED ) return
172+ }
173+
138174 const wrappedNext = typeof next === 'function' ? wrapNext ( req , meta . name , next ) : next
139- enterChannel . publish ( { name : meta . name , req, route : resolveLayerRoute ( meta , this ) , layer : this } )
175+ enterChannel . publish ( { name : meta . name , req, route, layer : this } )
140176
141177 try {
142178 return originalRequest . call ( this , req , res , wrappedNext )
@@ -146,15 +182,25 @@ function createLayerDispatchWrappers (name) {
146182 }
147183 }
148184
185+ /**
186+ * @param {Function } originalError
187+ * @returns {Function }
188+ */
149189 function wrapLayerError ( originalError ) {
150190 return function ( error , req , res , next ) {
151191 if ( ! enterChannel . hasSubscribers ) return originalError . call ( this , error , req , res , next )
152192
153193 const meta = getLayerMeta ( this )
154194 if ( meta === undefined || this . handle . length !== 4 ) return originalError . call ( this , error , req , res , next )
155195
196+ let route = meta . captureRoute
197+ if ( meta . needMultiMatch ) {
198+ route = resolveLayerRouteOrForwardError ( meta , this , next )
199+ if ( route === ROUTE_RESOLUTION_FAILED ) return
200+ }
201+
156202 const wrappedNext = typeof next === 'function' ? wrapNext ( req , meta . name , next ) : next
157- enterChannel . publish ( { name : meta . name , req, route : resolveLayerRoute ( meta , this ) , layer : this } )
203+ enterChannel . publish ( { name : meta . name , req, route, layer : this } )
158204
159205 try {
160206 return originalError . call ( this , error , req , res , wrappedNext )
@@ -164,36 +210,163 @@ function createLayerDispatchWrappers (name) {
164210 }
165211 }
166212
167- // express <4.6.0 has no `Layer` prototype dispatch: the router invokes
168- // `layer.handle` directly and routes errors by its arity. There the handle is
169- // replaced in place, with the arity preserved so the host still routes
170- // correctly. Newer express, express 5, and the router package keep `handle`
171- // pristine and are traced through the prototype wraps above.
172- function wrapLegacyHandle ( layer , original ) {
213+ // express <4.6.0 dispatches `layer.handle` directly, so its replacement must preserve arity.
214+ /**
215+ * @param {object } layer
216+ * @param {Function } original
217+ * @returns {Function }
218+ */
219+ function wrapNativeLegacyRequestHandle ( layer , original ) {
220+ const meta = getLayerMeta ( layer )
221+ const { name, captureRoute, needMultiMatch } = meta
222+ return shimmer . wrapFunction ( original , inner => function ( req , res , next ) {
223+ if ( ! enterChannel . hasSubscribers ) return inner . call ( this , req , res , next )
224+
225+ let calls = 0
226+ if ( typeof next === 'function' ) {
227+ next = shimmer . wrapCallback ( next , originalNext => function next ( error ) {
228+ calls ++
229+ if ( calls === 1 ) {
230+ if ( error && error !== 'route' && error !== 'router' ) {
231+ publishError ( { req, error } )
232+ }
233+
234+ nextChannel . publish ( { req } )
235+ finishChannel . publish ( { req } )
236+ } else if ( calls === 2 ) {
237+ repeatChannel . publish ( { req, name, error } )
238+ }
239+
240+ originalNext . apply ( this , arguments )
241+ } )
242+ }
243+
244+ const route = needMultiMatch ? resolveLayerRoute ( meta , layer ) : captureRoute
245+ enterChannel . publish ( { name, req, route, layer } )
246+
247+ try {
248+ return inner . call ( this , req , res , next )
249+ } catch ( error ) {
250+ if ( calls === 0 ) {
251+ calls = 1
252+ publishError ( { req, error } )
253+ nextChannel . publish ( { req } )
254+ finishChannel . publish ( { req } )
255+ }
256+
257+ throw error
258+ } finally {
259+ exitChannel . publish ( { req } )
260+ }
261+ } )
262+ }
263+
264+ /**
265+ * @param {object } layer
266+ * @param {Function } original
267+ * @returns {Function }
268+ */
269+ function wrapNativeLegacyErrorHandle ( layer , original ) {
270+ const meta = getLayerMeta ( layer )
271+ const { name, captureRoute, needMultiMatch } = meta
272+ return shimmer . wrapFunction ( original , inner => function ( error , req , res , next ) {
273+ if ( ! enterChannel . hasSubscribers ) return inner . call ( this , error , req , res , next )
274+
275+ let calls = 0
276+ if ( typeof next === 'function' ) {
277+ next = shimmer . wrapCallback ( next , originalNext => function next ( nextError ) {
278+ calls ++
279+ if ( calls === 1 ) {
280+ if ( nextError && nextError !== 'route' && nextError !== 'router' ) {
281+ publishError ( { req, error : nextError } )
282+ }
283+
284+ nextChannel . publish ( { req } )
285+ finishChannel . publish ( { req } )
286+ } else if ( calls === 2 ) {
287+ repeatChannel . publish ( { req, name, error : nextError } )
288+ }
289+
290+ originalNext . apply ( this , arguments )
291+ } )
292+ }
293+
294+ const route = needMultiMatch ? resolveLayerRoute ( meta , layer ) : captureRoute
295+ enterChannel . publish ( { name, req, route, layer } )
296+
297+ try {
298+ return inner . call ( this , error , req , res , next )
299+ } catch ( caught ) {
300+ if ( calls === 0 ) {
301+ calls = 1
302+ publishError ( { req, error : caught } )
303+ nextChannel . publish ( { req } )
304+ finishChannel . publish ( { req } )
305+ }
306+
307+ throw caught
308+ } finally {
309+ exitChannel . publish ( { req } )
310+ }
311+ } )
312+ }
313+
314+ /**
315+ * @param {object } layer
316+ * @param {Function } original
317+ * @param {boolean } [guardRepeatedNext]
318+ * @returns {Function }
319+ */
320+ function wrapLegacyHandle ( layer , original , guardRepeatedNext = false ) {
321+ if ( ! guardRepeatedNext ) {
322+ return original . length === 4
323+ ? wrapNativeLegacyErrorHandle ( layer , original )
324+ : wrapNativeLegacyRequestHandle ( layer , original )
325+ }
326+
173327 // `annotateLayer` always runs first in `wrapStack`, so the captured meta is
174328 // never undefined here (unlike the prototype wraps, where `this` can be any
175329 // layer the host dispatches).
176330 const meta = getLayerMeta ( layer )
331+ const { name, captureRoute, needMultiMatch } = meta
177332 const wrapped = shimmer . wrapFunction ( original , inner => function ( ...args ) {
178333 if ( ! enterChannel . hasSubscribers ) return inner . apply ( this , args )
179334
180335 const isErrorHandler = original . length === 4
181336 const req = args [ isErrorHandler ? 1 : 0 ]
182337 const nextIndex = isErrorHandler ? 3 : 2
183- if ( typeof args [ nextIndex ] === 'function' ) args [ nextIndex ] = wrapNext ( req , meta . name , args [ nextIndex ] )
338+ let calls = 0
339+ if ( typeof args [ nextIndex ] === 'function' ) {
340+ args [ nextIndex ] = shimmer . wrapCallback ( args [ nextIndex ] , originalNext => function next ( error ) {
341+ calls ++
342+ if ( calls === 1 ) {
343+ if ( error && error !== 'route' && error !== 'router' ) {
344+ publishError ( { req, error } )
345+ }
184346
185- enterChannel . publish ( { name : meta . name , req, route : resolveLayerRoute ( meta , layer ) , layer } )
347+ nextChannel . publish ( { req } )
348+ finishChannel . publish ( { req } )
349+ } else if ( calls === 2 ) {
350+ repeatChannel . publish ( { req, name, error } )
351+ }
352+
353+ originalNext . apply ( this , arguments )
354+ } )
355+ }
356+
357+ const route = needMultiMatch ? resolveLayerRoute ( meta , layer ) : captureRoute
358+ enterChannel . publish ( { name, req, route, layer } )
186359
187360 try {
188361 return inner . apply ( this , args )
189362 } catch ( error ) {
190- // Unlike the prototype hosts, this router catches a synchronous throw
191- // outside the layer and calls its own `next(error)`, never `wrappedNext`.
192- // Mirror `wrapNext` here so the throwing layer still tags its error and
193- // finishes, rather than lingering on the stack until request finish.
194- publishError ( { req, error } )
195- nextChannel . publish ( { req } )
196- finishChannel . publish ( { req } )
363+ // Legacy hosts catch outside the layer and never call its wrapped `next`, so finish before rethrowing.
364+ if ( calls === 0 ) {
365+ calls = 1
366+ publishError ( { req , error } )
367+ nextChannel . publish ( { req } )
368+ finishChannel . publish ( { req } )
369+ }
197370
198371 throw error
199372 } finally {
@@ -209,6 +382,7 @@ function createLayerDispatchWrappers (name) {
209382
210383/**
211384 * @param {{ handle_request?: unknown, handleRequest?: unknown } } layer
385+ * @returns {boolean }
212386 */
213387function hasLayerDispatch ( layer ) {
214388 return typeof layer . handle_request === 'function' || typeof layer . handleRequest === 'function'
@@ -221,9 +395,11 @@ function hasLayerDispatch (layer) {
221395 * Host-resolved path-to-regexp compile adapter, or undefined when the host
222396 * instance ships no path-to-regexp. Captured here so each express/router
223397 * instance keeps the dialect it actually loaded.
224- * @param {((layer: object, original: Function) => Function) | undefined } [wrapLegacyHandle]
398+ * @param {((layer: object, original: Function, guardRepeatedNext?: boolean) => Function) | undefined }
399+ * [wrapLegacyHandle]
225400 * Fallback that replaces `layer.handle` for hosts without a `Layer` prototype
226401 * dispatch (express <4.6.0). Omitted for hosts that always ship one.
402+ * @returns {(original: Function) => Function }
227403 */
228404function createWrapRouterMethod ( name , compile , wrapLegacyHandle ) {
229405 const routeAddedChannel = channel ( `apm:${ name } :route:added` )
@@ -234,7 +410,7 @@ function createWrapRouterMethod (name, compile, wrapLegacyHandle) {
234410
235411 if ( wrapLegacyHandle !== undefined && ! hasLayerDispatch ( layer ) ) {
236412 if ( layer . __handle ) { // express-async-errors
237- layer . __handle = wrapLegacyHandle ( layer , layer . __handle )
413+ layer . __handle = wrapLegacyHandle ( layer , layer . __handle , true )
238414 } else {
239415 layer . handle = wrapLegacyHandle ( layer , layer . handle )
240416 }
0 commit comments