@@ -64,6 +64,8 @@ type Layer = {
64
64
handle_request : ( req : PatchedRequest , res : ExpressResponse , next : ( ) => void ) => void ;
65
65
route ?: { path : RouteType | RouteType [ ] } ;
66
66
path ?: string ;
67
+ regexp ?: RegExp ;
68
+ keys ?: { name : string ; offset : number ; optional : boolean } [ ] ;
67
69
} ;
68
70
69
71
type RouteType = string | RegExp ;
@@ -318,7 +320,24 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
318
320
}
319
321
320
322
// 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
+ }
322
341
323
342
// Normalize the partial route so that it doesn't contain leading or trailing slashes
324
343
// and exclude empty or '*' wildcard routes.
@@ -370,6 +389,79 @@ type LayerRoutePathInfo = {
370
389
numExtraSegments : number ;
371
390
} ;
372
391
392
+ /**
393
+ * Recreate layer.route.path from layer.regexp and layer.keys.
394
+ * Works until express.js used package [email protected]
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
+
373
465
/**
374
466
* Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`),
375
467
* a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally
@@ -382,11 +474,24 @@ type LayerRoutePathInfo = {
382
474
* if the route was an array (defaults to 0).
383
475
*/
384
476
function getLayerRoutePathInfo ( layer : Layer ) : LayerRoutePathInfo {
385
- const lrp = layer . route ?. path ;
477
+ let lrp = layer . route ?. path ;
386
478
387
479
const isRegex = isRegExp ( lrp ) ;
388
480
const isArray = Array . isArray ( lrp ) ;
389
481
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
+
390
495
if ( ! lrp ) {
391
496
return { isRegex, isArray, numExtraSegments : 0 } ;
392
497
}
@@ -424,3 +529,28 @@ function getLayerRoutePathString(isArray: boolean, lrp?: RouteType | RouteType[]
424
529
}
425
530
return lrp && lrp . toString ( ) ;
426
531
}
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