From 5b5e817b6169adebecfd3984197c0afb5f38f3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Wed, 25 Feb 2026 06:21:13 +0300 Subject: [PATCH 1/5] fix(mcp): split header value only on first colon in headerParser The headerParser function used arg.split(':') which splits on ALL colons in the string. JavaScript destructuring only captures the first two array elements, silently discarding content after the second colon. This caused header values containing colons (URLs with ://, port numbers, Base64 strings) to be silently truncated. The fix uses indexOf(':') + substring() to split only on the first colon, matching the HTTP header spec (RFC 7230 Section 3.2). Fixes microsoft/playwright-mcp#1417 --- packages/playwright-core/src/mcp/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/mcp/config.ts b/packages/playwright-core/src/mcp/config.ts index 495d2d9a911a4..a86dedd7717b5 100644 --- a/packages/playwright-core/src/mcp/config.ts +++ b/packages/playwright-core/src/mcp/config.ts @@ -432,7 +432,11 @@ export function headerParser(arg: string | undefined, previous?: Record = previous || {}; - const [name, value] = arg.split(':').map(v => v.trim()); + const colonIndex = arg.indexOf(':'); + if (colonIndex === -1) + return result; + const name = arg.substring(0, colonIndex).trim(); + const value = arg.substring(colonIndex + 1).trim(); result[name] = value; return result; } From 3519e1535e25408681a7bd311a7782f886dfce5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Wed, 25 Feb 2026 06:25:54 +0300 Subject: [PATCH 2/5] test(mcp): add unit tests for headerParser colon handling Add comprehensive tests for the headerParser function to verify that header values containing colons (URLs, multi-colon values) are preserved correctly. Fixes microsoft/playwright-mcp#1417 --- tests/mcp/header-parser.spec.ts | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/mcp/header-parser.spec.ts diff --git a/tests/mcp/header-parser.spec.ts b/tests/mcp/header-parser.spec.ts new file mode 100644 index 0000000000000..fdb4924e90ad9 --- /dev/null +++ b/tests/mcp/header-parser.spec.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '@playwright/test'; +import { headerParser } from '../../packages/playwright/src/mcp/browser/config'; + +test.describe('headerParser', () => { + test('should parse simple header', () => { + expect(headerParser('X-Custom: value')).toEqual({ 'X-Custom': 'value' }); + }); + + test('should preserve colons in header values containing URLs', () => { + expect(headerParser('X-Custom: http://example.com')).toEqual({ 'X-Custom': 'http://example.com' }); + }); + + test('should preserve colons in header values with multiple colons', () => { + expect(headerParser('X-Forwarded-Proto: value:with:colons')).toEqual({ 'X-Forwarded-Proto': 'value:with:colons' }); + }); + + test('should return previous or empty object for undefined input', () => { + expect(headerParser(undefined)).toEqual({}); + expect(headerParser(undefined, { 'Existing': 'header' })).toEqual({ 'Existing': 'header' }); + }); + + test('should return previous or empty object for empty string', () => { + expect(headerParser('')).toEqual({}); + }); + + test('should skip headers without colons', () => { + expect(headerParser('no-colon-header')).toEqual({}); + }); + + test('should trim whitespace from name and value', () => { + expect(headerParser(' Name : Value ')).toEqual({ 'Name': 'Value' }); + }); + + test('should accumulate headers with previous', () => { + const previous = { 'First': 'one' }; + expect(headerParser('Second: two', previous)).toEqual({ 'First': 'one', 'Second': 'two' }); + }); +}); From c95e384511db6d79c9cebfe00ef0af82a3905936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Thu, 5 Mar 2026 23:34:54 +0000 Subject: [PATCH 3/5] fix(mcp): support empty header values and fix colon truncation --- packages/playwright-core/src/mcp/config.ts | 10 ++-- tests/mcp/cdp.spec.ts | 26 +++++++++++ tests/mcp/header-parser.spec.ts | 54 ---------------------- 3 files changed, 31 insertions(+), 59 deletions(-) delete mode 100644 tests/mcp/header-parser.spec.ts diff --git a/packages/playwright-core/src/mcp/config.ts b/packages/playwright-core/src/mcp/config.ts index a86dedd7717b5..2caff85df52c2 100644 --- a/packages/playwright-core/src/mcp/config.ts +++ b/packages/playwright-core/src/mcp/config.ts @@ -332,7 +332,7 @@ export async function loadConfig(configFile: string | undefined): Promise(obj: T | undefined): Partial { return Object.fromEntries( - Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined) + Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined) ) as Partial; } @@ -433,10 +433,10 @@ export function headerParser(arg: string | undefined, previous?: Record = previous || {}; const colonIndex = arg.indexOf(':'); - if (colonIndex === -1) - return result; - const name = arg.substring(0, colonIndex).trim(); - const value = arg.substring(colonIndex + 1).trim(); + + const name = colonIndex === -1 ? arg.trim() : arg.substring(0, colonIndex).trim(); + const value = colonIndex === -1 ? '' : arg.substring(colonIndex + 1).trim(); + result[name] = value; return result; } diff --git a/tests/mcp/cdp.spec.ts b/tests/mcp/cdp.spec.ts index f05ea7c306421..43a05c9a7fb4a 100644 --- a/tests/mcp/cdp.spec.ts +++ b/tests/mcp/cdp.spec.ts @@ -110,3 +110,29 @@ test('cdp server with headers', async ({ startClient, server }) => { }); expect(authHeader).toBe('Bearer 1234567890'); }); + +test('cdp server with empty and complex headers', async ({ startClient, server }) => { + let customHeader = ''; + let emptyHeader = ''; + server.setRoute('/json/version/', (req, res) => { + customHeader = req.headers['x-forwarded-proto'] as string; + emptyHeader = req.headers['x-empty'] as string; + res.end(); + }); + + const { client } = await startClient({ + args: [ + `--cdp-endpoint=${server.PREFIX}`, + '--cdp-header', 'X-Forwarded-Proto: value:with:colons', + '--cdp-header', 'X-Empty' + ] + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + isError: true, + }); + expect(customHeader).toBe('value:with:colons'); + expect(emptyHeader).toBe(''); +}); diff --git a/tests/mcp/header-parser.spec.ts b/tests/mcp/header-parser.spec.ts deleted file mode 100644 index fdb4924e90ad9..0000000000000 --- a/tests/mcp/header-parser.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from '@playwright/test'; -import { headerParser } from '../../packages/playwright/src/mcp/browser/config'; - -test.describe('headerParser', () => { - test('should parse simple header', () => { - expect(headerParser('X-Custom: value')).toEqual({ 'X-Custom': 'value' }); - }); - - test('should preserve colons in header values containing URLs', () => { - expect(headerParser('X-Custom: http://example.com')).toEqual({ 'X-Custom': 'http://example.com' }); - }); - - test('should preserve colons in header values with multiple colons', () => { - expect(headerParser('X-Forwarded-Proto: value:with:colons')).toEqual({ 'X-Forwarded-Proto': 'value:with:colons' }); - }); - - test('should return previous or empty object for undefined input', () => { - expect(headerParser(undefined)).toEqual({}); - expect(headerParser(undefined, { 'Existing': 'header' })).toEqual({ 'Existing': 'header' }); - }); - - test('should return previous or empty object for empty string', () => { - expect(headerParser('')).toEqual({}); - }); - - test('should skip headers without colons', () => { - expect(headerParser('no-colon-header')).toEqual({}); - }); - - test('should trim whitespace from name and value', () => { - expect(headerParser(' Name : Value ')).toEqual({ 'Name': 'Value' }); - }); - - test('should accumulate headers with previous', () => { - const previous = { 'First': 'one' }; - expect(headerParser('Second: two', previous)).toEqual({ 'First': 'one', 'Second': 'two' }); - }); -}); From 2b0199a8a5f57d03f30e5eab6c32eaa24815646a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Sat, 7 Mar 2026 13:11:03 +0000 Subject: [PATCH 4/5] fix(mcp): guard against empty header names in headerParser --- packages/playwright-core/src/mcp/config.ts | 4 ++++ tests/mcp/cdp.spec.ts | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/playwright-core/src/mcp/config.ts b/packages/playwright-core/src/mcp/config.ts index 2caff85df52c2..719767010c4f3 100644 --- a/packages/playwright-core/src/mcp/config.ts +++ b/packages/playwright-core/src/mcp/config.ts @@ -437,6 +437,10 @@ export function headerParser(arg: string | undefined, previous?: Record { + server.setRoute('/json/version/', (req, res) => { + res.end(); + }); + + const { client } = await startClient({ + args: [ + `--cdp-endpoint=${server.PREFIX}`, + '--cdp-header', ':', + '--cdp-header', ':value', + '--cdp-header', 'Valid-Header: valid-value' + ] + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + isError: true, + }); +}); From 189e31ad753b14076e7d959b18939e416c3c2bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Mon, 9 Mar 2026 20:00:44 +0000 Subject: [PATCH 5/5] revert(mcp): remove out-of-scope empty header name guard and test --- packages/playwright-core/src/mcp/config.ts | 7 +------ tests/mcp/cdp.spec.ts | 21 --------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/packages/playwright-core/src/mcp/config.ts b/packages/playwright-core/src/mcp/config.ts index 719767010c4f3..577da213a873e 100644 --- a/packages/playwright-core/src/mcp/config.ts +++ b/packages/playwright-core/src/mcp/config.ts @@ -332,7 +332,7 @@ export async function loadConfig(configFile: string | undefined): Promise(obj: T | undefined): Partial { return Object.fromEntries( - Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined) + Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined) ) as Partial; } @@ -436,11 +436,6 @@ export function headerParser(arg: string | undefined, previous?: Record { - server.setRoute('/json/version/', (req, res) => { - res.end(); - }); - - const { client } = await startClient({ - args: [ - `--cdp-endpoint=${server.PREFIX}`, - '--cdp-header', ':', - '--cdp-header', ':value', - '--cdp-header', 'Valid-Header: valid-value' - ] - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - isError: true, - }); -});