From 267f096a53f93ba41ef44c0fb4862d1d54e2a450 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 25 Feb 2025 12:33:51 -0800 Subject: [PATCH 1/3] fix: do not change glob pattern when converting to url Fixes https://github.com/microsoft/playwright/issues/34915 --- .../src/utils/isomorphic/urlMatch.ts | 16 +++++++++++++++- tests/page/interception.spec.ts | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index 1d3bd011dc..fa2c92bbbd 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -101,7 +101,21 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match // Allow http(s) baseURL to match ws(s) urls. if (baseURL && /^https?:\/\//.test(baseURL) && /^wss?:\/\//.test(urlString)) baseURL = baseURL.replace(/^http/, 'ws'); - match = constructURLBasedOnBaseURL(baseURL, match); + if (baseURL) { + // String starting with a dot are treated as explicit relative URL. + if (match.startsWith('.')) { + match = constructURLBasedOnBaseURL(baseURL, match); + } else { + // We cannot pass `match` as the relative URL as regex symbols would be misinterpreted. + const relativeBase = match.startsWith('/') ? '/' : '.'; + let prefix = constructURLBasedOnBaseURL(baseURL, relativeBase); + if (prefix !== relativeBase) { + if (match.startsWith('/') && prefix.endsWith('/')) + prefix = prefix.substring(0, prefix.length - 1); + match = prefix + match; + } + } + } } if (isString(match)) match = globToRegex(match); diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index d3443f9015..891adeaff0 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -16,7 +16,7 @@ */ import { test as it, expect } from './pageTest'; -import { globToRegex } from '../../packages/playwright-core/lib/utils/isomorphic/urlMatch'; +import { globToRegex, urlMatches } from '../../packages/playwright-core/lib/utils/isomorphic/urlMatch'; import vm from 'vm'; it('should work with navigation @smoke', async ({ page, server }) => { @@ -107,6 +107,20 @@ it('should work with glob', async () => { expect(globToRegex('$^+.\\*()|\\?\\{\\}\\[\\]')).toEqual(/^\$\^\+\.\*\(\)\|\?\{\}\[\]$/); }); +it('should intercept by glob', async function({ page, server, isAndroid }) { + it.skip(isAndroid); + + await page.goto(server.EMPTY_PAGE); + await page.route('http://localhos*?/?oo', async route => { + await route.fulfill({ + status: 200, + body: 'intercepted', + }); + }); + const result = await page.evaluate(url => fetch(url).then(r => r.text()), server.PREFIX + '/foo'); + expect(result).toBe('intercepted'); +}); + it('should intercept network activity from worker', async function({ page, server, isAndroid }) { it.skip(isAndroid); From 02a2180ccf04b5df5699e95143279a201b7416ff Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 25 Feb 2025 14:30:38 -0800 Subject: [PATCH 2/3] use another heuristic --- .../src/utils/isomorphic/urlMatch.ts | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index fa2c92bbbd..a519314f37 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -101,24 +101,18 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match // Allow http(s) baseURL to match ws(s) urls. if (baseURL && /^https?:\/\//.test(baseURL) && /^wss?:\/\//.test(urlString)) baseURL = baseURL.replace(/^http/, 'ws'); - if (baseURL) { - // String starting with a dot are treated as explicit relative URL. - if (match.startsWith('.')) { - match = constructURLBasedOnBaseURL(baseURL, match); - } else { - // We cannot pass `match` as the relative URL as regex symbols would be misinterpreted. - const relativeBase = match.startsWith('/') ? '/' : '.'; - let prefix = constructURLBasedOnBaseURL(baseURL, relativeBase); - if (prefix !== relativeBase) { - if (match.startsWith('/') && prefix.endsWith('/')) - prefix = prefix.substring(0, prefix.length - 1); - match = prefix + match; - } - } - } + // Resolve match relative to baseURL only if baseURL is set and match is not an absolute URL. + // Otherwise, leave it unchanged to prevent the URL constructor from interpreting glob symbols + // like ?, {, and }, which could alter the pattern. + if (baseURL && !parseURL(match)) + match = constructURLBasedOnBaseURL(baseURL, match); } - if (isString(match)) + if (isString(match)) { + const tryWithoutTrailingSlash = urlString.endsWith('/') && !match.endsWith('/'); match = globToRegex(match); + if (tryWithoutTrailingSlash && match.test(urlString.substring(0, urlString.length - 1))) + return true; + } if (isRegExp(match)) return match.test(urlString); const url = parseURL(urlString); From ffcb43927505d1e441088fac536dc56af0349283 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 27 Feb 2025 08:43:26 -0800 Subject: [PATCH 3/3] map tokens --- .../src/utils/isomorphic/urlMatch.ts | 40 ++++++++++++++----- tests/page/interception.spec.ts | 12 +++++- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index a519314f37..55fc6c7d72 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -101,18 +101,38 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match // Allow http(s) baseURL to match ws(s) urls. if (baseURL && /^https?:\/\//.test(baseURL) && /^wss?:\/\//.test(urlString)) baseURL = baseURL.replace(/^http/, 'ws'); - // Resolve match relative to baseURL only if baseURL is set and match is not an absolute URL. - // Otherwise, leave it unchanged to prevent the URL constructor from interpreting glob symbols - // like ?, {, and }, which could alter the pattern. - if (baseURL && !parseURL(match)) - match = constructURLBasedOnBaseURL(baseURL, match); + + const tokenMap = new Map(); + function mapToken(original: string, replacement: string) { + tokenMap.set(replacement, original); + return replacement; + } + // Glob symbols may be escaped in the URL and some of them such as ? affect resolution, + // so we replace them with safe components first. + const relativePath = match.split('/').map((token, index) => { + if (token === '.' || token === '..' || token === '') + return token; + // Handle special case of http*:// + if (index === 0 && token.endsWith(':')) { + return mapToken(token, 'http:'); + } else { + const questionIndex = token.indexOf('?'); + if (questionIndex === -1) + return mapToken(token, `$_${index}_$`); + if (questionIndex === 0) + return mapToken(token, `?$_${index}_$`); + const newPrefix = mapToken(token.substring(0, questionIndex), `$_${index}_$`); + const newSuffix = mapToken(token.substring(questionIndex), `?$_${index}_$`); + return newPrefix + newSuffix; + } + }).join('/'); + let resolved = constructURLBasedOnBaseURL(baseURL, relativePath); + for (const [token, original] of tokenMap) + resolved = resolved.replace(token, original); + match = resolved; } - if (isString(match)) { - const tryWithoutTrailingSlash = urlString.endsWith('/') && !match.endsWith('/'); + if (isString(match)) match = globToRegex(match); - if (tryWithoutTrailingSlash && match.test(urlString.substring(0, urlString.length - 1))) - return true; - } if (isRegExp(match)) return match.test(urlString); const url = parseURL(urlString); diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 891adeaff0..c93d7c3233 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -105,13 +105,23 @@ it('should work with glob', async () => { expect(globToRegex('\\[')).toEqual(/^\[$/); expect(globToRegex('[a-z]')).toEqual(/^[a-z]$/); expect(globToRegex('$^+.\\*()|\\?\\{\\}\\[\\]')).toEqual(/^\$\^\+\.\*\(\)\|\?\{\}\[\]$/); + + expect(urlMatches(undefined, 'http://playwright.dev/', 'http://playwright.dev')).toBeTruthy(); + expect(urlMatches(undefined, 'http://playwright.dev/?a=b', 'http://playwright.dev?a=b')).toBeTruthy(); + expect(urlMatches(undefined, 'http://playwright.dev/', 'h*://playwright.dev')).toBeTruthy(); + expect(urlMatches(undefined, 'http://api.playwright.dev/?x=y', 'http://*.playwright.dev?x=y')).toBeTruthy(); + expect(urlMatches(undefined, 'http://playwright.dev/foo/bar', '**/foo/**')).toBeTruthy(); + expect(urlMatches('http://playwright.dev/foo/', 'http://playwright.dev/foo/bar?x=y', './bar?x=y')).toBeTruthy(); + + // This is not supported, we treat ? as a query separator. + expect(urlMatches(undefined, 'http://playwright.dev/', 'http://playwright.?ev')).toBeFalsy(); }); it('should intercept by glob', async function({ page, server, isAndroid }) { it.skip(isAndroid); await page.goto(server.EMPTY_PAGE); - await page.route('http://localhos*?/?oo', async route => { + await page.route('http://localhos**/?oo', async route => { await route.fulfill({ status: 200, body: 'intercepted',