@@ -64,6 +64,8 @@ type Layer = {
6464 handle_request : ( req : PatchedRequest , res : ExpressResponse , next : ( ) => void ) => void ;
6565 route ?: { path : RouteType | RouteType [ ] } ;
6666 path ?: string ;
67+ regexp ?: RegExp ;
68+ keys ?: { name : string ; offset : number ; optional : boolean } [ ] ;
6769} ;
6870
6971type RouteType = string | RegExp ;
@@ -318,7 +320,24 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
318320 }
319321
320322 // Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
321- const partialRoute = layerRoutePath || layer . path || '' ;
323+ let partialRoute ;
324+
325+ if ( layerRoutePath ) {
326+ partialRoute = layerRoutePath ;
327+ } else {
328+ /**
329+ * prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path
330+ * example:
331+ * original url: /api/v1/1234
332+ * prevent: /api/api/v1/:userId
333+ * router structure
334+ * /api -> middleware
335+ * /api/v1 -> middleware
336+ * /1234 -> endpoint with param :userId
337+ * final _reconstructedRoute is /api/v1/:userId
338+ */
339+ partialRoute = preventDuplicateSegments ( req . originalUrl , req . _reconstructedRoute , layer . path ) || '' ;
340+ }
322341
323342 // Normalize the partial route so that it doesn't contain leading or trailing slashes
324343 // and exclude empty or '*' wildcard routes.
@@ -370,6 +389,79 @@ type LayerRoutePathInfo = {
370389 numExtraSegments : number ;
371390} ;
372391
392+ /**
393+ * Recreate layer.route.path from layer.regexp and layer.keys.
394+ * Works until express.js used package path-to-regexp@0.1.7
395+ * or until layer.keys contain offset attribute
396+ *
397+ * @param layer the layer to extract the stringified route from
398+ *
399+ * @returns string in layer.route.path structure 'router/:pathParam' or undefined
400+ */
401+ export const extractOriginalRoute = (
402+ path ?: Layer [ 'path' ] ,
403+ regexp ?: Layer [ 'regexp' ] ,
404+ keys ?: Layer [ 'keys' ] ,
405+ ) : string | undefined => {
406+ if ( ! path || ! regexp || ! keys || Object . keys ( keys ) . length === 0 || ! keys [ 0 ] ?. offset ) {
407+ return undefined ;
408+ }
409+
410+ const orderedKeys = keys . sort ( ( a , b ) => a . offset - b . offset ) ;
411+
412+ // add d flag for getting indices from regexp result
413+ const pathRegex = new RegExp ( regexp , `${ regexp . flags } d` ) ;
414+ /**
415+ * use custom type cause of TS error with missing indices in RegExpExecArray
416+ */
417+ const execResult = pathRegex . exec ( path ) as ( RegExpExecArray & { indices : [ number , number ] [ ] } ) | null ;
418+
419+ if ( ! execResult || ! execResult . indices ) {
420+ return undefined ;
421+ }
422+ /**
423+ * remove first match from regex cause contain whole layer.path
424+ */
425+ const [ , ...paramIndices ] = execResult . indices ;
426+
427+ if ( paramIndices . length !== orderedKeys . length ) {
428+ return undefined ;
429+ }
430+ let resultPath = path ;
431+ let indexShift = 0 ;
432+
433+ /**
434+ * iterate param matches from regexp.exec
435+ */
436+ paramIndices . forEach ( ( [ startOffset , endOffset ] , index : number ) => {
437+ /**
438+ * isolate part before param
439+ */
440+ const substr1 = resultPath . substring ( 0 , startOffset - indexShift ) ;
441+ /**
442+ * define paramName as replacement in format :pathParam
443+ */
444+ const replacement = `:${ orderedKeys [ index ] . name } ` ;
445+
446+ /**
447+ * isolate part after param
448+ */
449+ const substr2 = resultPath . substring ( endOffset - indexShift ) ;
450+
451+ /**
452+ * recreate original path but with param replacement
453+ */
454+ resultPath = substr1 + replacement + substr2 ;
455+
456+ /**
457+ * calculate new index shift after resultPath was modified
458+ */
459+ indexShift = indexShift + ( endOffset - startOffset - replacement . length ) ;
460+ } ) ;
461+
462+ return resultPath ;
463+ } ;
464+
373465/**
374466 * Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`),
375467 * a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally
@@ -382,11 +474,24 @@ type LayerRoutePathInfo = {
382474 * if the route was an array (defaults to 0).
383475 */
384476function getLayerRoutePathInfo ( layer : Layer ) : LayerRoutePathInfo {
385- const lrp = layer . route ?. path ;
477+ let lrp = layer . route ?. path ;
386478
387479 const isRegex = isRegExp ( lrp ) ;
388480 const isArray = Array . isArray ( lrp ) ;
389481
482+ if ( ! lrp ) {
483+ // parse node.js major version
484+ const [ major ] = process . versions . node . split ( '.' ) . map ( Number ) ;
485+
486+ // allow call extractOriginalRoute only if node version support Regex d flag, node 16+
487+ if ( major >= 16 ) {
488+ /**
489+ * If lrp does not exist try to recreate original layer path from route regexp
490+ */
491+ lrp = extractOriginalRoute ( layer . path , layer . regexp , layer . keys ) ;
492+ }
493+ }
494+
390495 if ( ! lrp ) {
391496 return { isRegex, isArray, numExtraSegments : 0 } ;
392497 }
@@ -424,3 +529,28 @@ function getLayerRoutePathString(isArray: boolean, lrp?: RouteType | RouteType[]
424529 }
425530 return lrp && lrp . toString ( ) ;
426531}
532+
533+ /**
534+ * remove duplicate segment contain in layerPath against reconstructedRoute,
535+ * and return only unique segment that can be added into reconstructedRoute
536+ */
537+ export function preventDuplicateSegments (
538+ originalUrl ?: string ,
539+ reconstructedRoute ?: string ,
540+ layerPath ?: string ,
541+ ) : string | undefined {
542+ const originalUrlSplit = originalUrl ?. split ( '/' ) . filter ( v => ! ! v ) ;
543+ let tempCounter = 0 ;
544+ const currentOffset = reconstructedRoute ?. split ( '/' ) . filter ( v => ! ! v ) . length || 0 ;
545+ const result = layerPath
546+ ?. split ( '/' )
547+ . filter ( segment => {
548+ if ( originalUrlSplit ?. [ currentOffset + tempCounter ] === segment ) {
549+ tempCounter += 1 ;
550+ return true ;
551+ }
552+ return false ;
553+ } )
554+ . join ( '/' ) ;
555+ return result ;
556+ }
0 commit comments