Skip to content

Commit 585c5f7

Browse files
fix: make ?/# pre-processing conditional on template operators; don't throw on literal ? at construction
1 parent a911de3 commit 585c5f7

File tree

2 files changed

+34
-26
lines changed

2 files changed

+34
-26
lines changed

packages/core/src/shared/uriTemplate.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,6 @@ export class UriTemplate {
4343
UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, 'Template');
4444
this.template = template;
4545
this.parts = this.parse(template);
46-
47-
// Templates with a literal '?' in a string segment are incompatible
48-
// with match()'s split-at-'?' query parsing — the path regex would
49-
// include the escaped '?' but the URI is split before matching.
50-
// Supporting this requires a fragile two-code-path implementation
51-
// that has proven bug-prone, so reject at construction time.
52-
const literalQueryPart = this.parts.find(part => typeof part === 'string' && part.includes('?'));
53-
if (literalQueryPart !== undefined) {
54-
throw new Error(
55-
`UriTemplate: literal '?' in template string is not supported. ` +
56-
`Use {?param} to introduce query parameters instead. Template: "${template}"`
57-
);
58-
}
5946
}
6047

6148
toString(): string {
@@ -297,14 +284,27 @@ export class UriTemplate {
297284
}
298285
}
299286

300-
// Strip any URI fragment before splitting path/query.
301-
const hashIndex = uri.indexOf('#');
302-
const uriNoFrag = hashIndex === -1 ? uri : uri.slice(0, hashIndex);
287+
// Only strip the URI fragment when the template has no {#var} operator;
288+
// otherwise the fragment is part of what the path regex must capture.
289+
const hasHashOperator = this.parts.some(p => typeof p !== 'string' && p.operator === '#');
290+
let working = uri;
291+
if (!hasHashOperator) {
292+
const hashIndex = working.indexOf('#');
293+
if (hashIndex !== -1) working = working.slice(0, hashIndex);
294+
}
303295

304-
// Split URI into path and query parts at the first '?'
305-
const queryIndex = uriNoFrag.indexOf('?');
306-
const pathPart = queryIndex === -1 ? uriNoFrag : uriNoFrag.slice(0, queryIndex);
307-
const queryPart = queryIndex === -1 ? '' : uriNoFrag.slice(queryIndex + 1);
296+
// Only split path/query when the template actually has {?..}/{&..}
297+
// operators. Otherwise match the path regex against the full URI so
298+
// {+var} can capture across '?' as it did before query-param support.
299+
let pathPart = working;
300+
let queryPart = '';
301+
if (queryParts.length > 0) {
302+
const queryIndex = working.indexOf('?');
303+
if (queryIndex !== -1) {
304+
pathPart = working.slice(0, queryIndex);
305+
queryPart = working.slice(queryIndex + 1);
306+
}
307+
}
308308

309309
pattern += '$';
310310
UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern');

packages/core/test/shared/uriTemplate.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,20 @@ describe('UriTemplate', () => {
256256
expect(template.match('/search?q=%ZZ')).toEqual({ q: '%ZZ' });
257257
});
258258

259-
it('should reject templates with literal ? in a string segment', () => {
260-
expect(() => new UriTemplate('/path?fixed=1')).toThrow(/literal '\?'/);
261-
expect(() => new UriTemplate('/path?static=1{?dynamic}')).toThrow(/literal '\?'/);
262-
expect(() => new UriTemplate('/path?static=1{&dynamic}')).toThrow(/literal '\?'/);
263-
expect(() => new UriTemplate('/api?v=2{&key,page}')).toThrow(/literal '\?'/);
264-
expect(() => new UriTemplate('/api/{version}?format=json{&key}')).toThrow(/literal '\?'/);
259+
it('should not throw on literal ? in a string segment (expand-only usage)', () => {
260+
expect(() => new UriTemplate('/path?fixed=1')).not.toThrow();
261+
expect(() => new UriTemplate('http://e.com/?literal').expand({})).not.toThrow();
262+
expect(new UriTemplate('http://e.com/?literal').expand({})).toBe('http://e.com/?literal');
263+
});
264+
265+
it('should let {+var} capture across ? when template has no query operators', () => {
266+
const template = new UriTemplate('http://e.com{+rest}');
267+
expect(template.match('http://e.com/search?q=hello')).toEqual({ rest: '/search?q=hello' });
268+
});
269+
270+
it('should let {#var} capture the fragment', () => {
271+
const template = new UriTemplate('/page{#section}');
272+
expect(template.match('/page#intro')).toEqual({ section: '#intro' });
265273
});
266274

267275
it('should accept templates using {?param} for query parameters', () => {

0 commit comments

Comments
 (0)