@@ -375,6 +375,85 @@ export function safeExtractMessageAndStackFromError(err: unknown): [errMessage:
375375 return ( err instanceof Error ) ? [ err . message , err . stack ] as const : [ "Unknown Error" , undefined ] as const ;
376376}
377377
378+ /**
379+ * Normalizes URL by removing trailing slashes and fixing double slashes.
380+ * Preserves the protocol (http://, https://) but removes trailing slashes from the rest.
381+ *
382+ * @param url The URL to normalize
383+ * @returns The normalized URL without trailing slashes
384+ */
385+ export function normalizeUrl ( url : string ) : string {
386+ if ( ! url || typeof url !== 'string' ) {
387+ return url ;
388+ }
389+
390+ // Trim whitespace
391+ url = url . trim ( ) ;
392+
393+ if ( ! url ) {
394+ return url ;
395+ }
396+
397+ // Remove trailing slash, but preserve protocol
398+ if ( url . endsWith ( '/' ) && ! url . match ( / ^ h t t p s ? : \/ \/ $ / ) ) {
399+ url = url . slice ( 0 , - 1 ) ;
400+ }
401+
402+ // Fix double slashes (except in protocol)
403+ url = url . replace ( / ( [ ^ : ] \/ ) \/ + / g, '$1' ) ;
404+
405+ return url ;
406+ }
407+
408+ /**
409+ * Normalizes a path pattern for custom request handlers.
410+ * Ensures both trailing slash and non-trailing slash versions are handled.
411+ *
412+ * @param pattern The original pattern from customRequestHandler attribute
413+ * @returns An array of patterns to match both with and without trailing slash
414+ */
415+ export function normalizeCustomHandlerPattern ( pattern : string ) : string [ ] {
416+ if ( ! pattern || typeof pattern !== 'string' ) {
417+ return [ pattern ] ;
418+ }
419+
420+ pattern = pattern . trim ( ) ;
421+
422+ if ( ! pattern ) {
423+ return [ pattern ] ;
424+ }
425+
426+ // If pattern already ends with optional trailing slash, return as-is
427+ if ( pattern . endsWith ( '/?$' ) || pattern . endsWith ( '/?)' ) ) {
428+ return [ pattern ] ;
429+ }
430+
431+ // If pattern ends with $, handle it specially
432+ if ( pattern . endsWith ( '$' ) ) {
433+ const basePattern = pattern . slice ( 0 , - 1 ) ;
434+
435+ // If already ends with slash, create both versions
436+ if ( basePattern . endsWith ( '/' ) ) {
437+ const withoutSlash = basePattern . slice ( 0 , - 1 ) + '$' ;
438+ const withSlash = pattern ;
439+ return [ withoutSlash , withSlash ] ;
440+ } else {
441+ // Add optional trailing slash
442+ const withSlash = basePattern + '/?$' ;
443+ return [ withSlash ] ;
444+ }
445+ }
446+
447+ // For patterns without $, add both versions
448+ if ( pattern . endsWith ( '/' ) ) {
449+ const withoutSlash = pattern . slice ( 0 , - 1 ) ;
450+ return [ withoutSlash , pattern ] ;
451+ } else {
452+ const withSlash = pattern + '/' ;
453+ return [ pattern , withSlash ] ;
454+ }
455+ }
456+
378457
379458export default {
380459 compareVersions,
@@ -400,6 +479,8 @@ export default {
400479 md5,
401480 newEntityId,
402481 normalize,
482+ normalizeCustomHandlerPattern,
483+ normalizeUrl,
403484 quoteRegex,
404485 randomSecureToken,
405486 randomString,
0 commit comments