@@ -105,7 +105,7 @@ export class UriTemplate {
105105 return expr
106106 . slice ( operator . length )
107107 . split ( ',' )
108- . map ( name => name . replace ( '*' , '' ) . trim ( ) )
108+ . map ( name => name . replace ( / \* / g , '' ) . trim ( ) )
109109 . filter ( name => name . length > 0 ) ;
110110 }
111111
@@ -256,10 +256,11 @@ export class UriTemplate {
256256 match ( uri : string ) : Variables | null {
257257 UriTemplate . validateLength ( uri , MAX_TEMPLATE_LENGTH , 'URI' ) ;
258258
259- // Split URI into path and query parts
260- const queryIndex = uri . indexOf ( '?' ) ;
261- const pathPart = queryIndex === - 1 ? uri : uri . slice ( 0 , queryIndex ) ;
262- const queryPart = queryIndex === - 1 ? '' : uri . slice ( queryIndex + 1 ) ;
259+ // Check whether any literal string segment in the template contains a
260+ // '?'. If so, the template author has written a manual query-string
261+ // prefix (e.g. `/path?fixed=1{&var}`) and we cannot simply split the
262+ // URI at its first '?' — the path regex itself needs to consume past it.
263+ const hasLiteralQuery = this . parts . some ( part => typeof part === 'string' && part . includes ( '?' ) ) ;
263264
264265 // Build regex pattern for path (non-query) parts
265266 let pattern = '^' ;
@@ -282,18 +283,36 @@ export class UriTemplate {
282283 }
283284 }
284285
285- pattern += '$' ;
286- UriTemplate . validateLength ( pattern , MAX_REGEX_LENGTH , 'Generated regex pattern' ) ;
287- const regex = new RegExp ( pattern ) ;
288- const match = pathPart . match ( regex ) ;
289-
290- if ( ! match ) return null ;
286+ let match : RegExpMatchArray | null ;
287+ let queryPart : string ;
288+
289+ if ( hasLiteralQuery ) {
290+ // Match the path regex against the full URI without a trailing
291+ // anchor, then treat everything after the match as the remaining
292+ // query string to parse for {?...}/{&...} expressions.
293+ UriTemplate . validateLength ( pattern , MAX_REGEX_LENGTH , 'Generated regex pattern' ) ;
294+ const regex = new RegExp ( pattern ) ;
295+ match = uri . match ( regex ) ;
296+ if ( ! match ) return null ;
297+ queryPart = uri . slice ( match [ 0 ] . length ) . replace ( / ^ & / , '' ) ;
298+ } else {
299+ // Split URI into path and query parts at the first '?'
300+ const queryIndex = uri . indexOf ( '?' ) ;
301+ const pathPart = queryIndex === - 1 ? uri : uri . slice ( 0 , queryIndex ) ;
302+ queryPart = queryIndex === - 1 ? '' : uri . slice ( queryIndex + 1 ) ;
303+
304+ pattern += '$' ;
305+ UriTemplate . validateLength ( pattern , MAX_REGEX_LENGTH , 'Generated regex pattern' ) ;
306+ const regex = new RegExp ( pattern ) ;
307+ match = pathPart . match ( regex ) ;
308+ if ( ! match ) return null ;
309+ }
291310
292311 const result : Variables = { } ;
293312
294313 for ( const [ i , { name, exploded } ] of names . entries ( ) ) {
295314 const value = match [ i + 1 ] ! ;
296- const cleanName = name . replace ( '*' , '' ) ;
315+ const cleanName = name . replace ( / \* / g , '' ) ;
297316 result [ cleanName ] = exploded && value . includes ( ',' ) ? value . split ( ',' ) : value ;
298317 }
299318
@@ -311,7 +330,7 @@ export class UriTemplate {
311330 }
312331
313332 for ( const { name, exploded } of queryParts ) {
314- const cleanName = name . replace ( '*' , '' ) ;
333+ const cleanName = name . replace ( / \* / g , '' ) ;
315334 const value = queryParams . get ( cleanName ) ;
316335
317336 if ( value === undefined ) continue ;
0 commit comments