feat: Add support for extra HTTP headers in Browser Agent
- Add browser_http_headers field to AgentConfig for custom HTTP headers - Implement UI settings field with textarea input for header configuration - Add validation function to sanitize and validate HTTP headers - Integrate headers into browser session initialization with error handling - Support KEY=VALUE format in settings UI for easy header configuration - Add safety checks to prevent dangerous headers that could break browser functionality Resolves #657
kaghatim committed
Aug 21, 2025 at 15:13 UTC
e5d924a8cb291ceb6af10290a3d218a1beffe6e4
4 files changed
+74
-2
agent.py
+1
@@ -221,6 +221,7 @@ class AgentConfig:
221
profile: str = ""
222
memory_subdir: str = ""
223
knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
224
+ browser_http_headers: dict[str, str] = field(default_factory=dict) # Custom HTTP headers for browser requests
225
code_exec_ssh_enabled: bool = True
226
code_exec_ssh_addr: str = "localhost"
227
code_exec_ssh_port: int = 55022
initialize.py
+1
@@ -79,6 +79,7 @@ def initialize_agent():
79
memory_subdir=current_settings["agent_memory_subdir"],
80
knowledge_subdirs=[current_settings["agent_knowledge_subdir"], "default"],
81
mcp_servers=current_settings["mcp_servers"],
82
+ browser_http_headers=current_settings["browser_http_headers"],
83
# code_exec params get initialized in _set_runtime_config
84
# additional = {},
85
)
python/helpers/settings.py
+59
-1
@@ -52,6 +52,7 @@ class Settings(TypedDict):
52
browser_model_rl_input: int
53
browser_model_rl_output: int
54
browser_model_kwargs: dict[str, str]
55
+ browser_http_headers: dict[str, str]
56
57
agent_profile: str
58
agent_memory_subdir: str
@@ -503,6 +504,16 @@ def convert_out(settings: Settings) -> SettingsOutput:
504
}
505
)
506
507
+ browser_model_fields.append(
508
+ {
509
+ "id": "browser_http_headers",
510
+ "title": "HTTP Headers",
511
+ "description": "HTTP headers to include with all browser requests. Format is KEY=VALUE on individual lines, just like .env file. Example: Authorization=Bearer token123",
512
+ "type": "textarea",
513
+ "value": _dict_to_env(settings.get("browser_http_headers", {})),
514
+ }
515
+ )
516
+
517
browser_model_section: SettingsSection = {
518
"id": "browser_model",
519
"title": "Web Browser Model",
@@ -1213,7 +1224,14 @@ def convert_in(settings: dict) -> Settings:
1224
)
1225
1226
if not should_skip:
1216
- if field["id"].endswith("_kwargs"):
1227
+ # Special handling for browser_http_headers
1228
+ if field["id"] == "browser_http_headers":
1229
+ headers_dict = _env_to_dict(field["value"])
1230
+ # Validate headers before saving
1231
+ validated_headers = _validate_http_headers(headers_dict)
1232
+ current[field["id"]] = validated_headers
1233
+ PrintStyle().info(f"Set browser_http_headers: {validated_headers}")
1234
+ elif field["id"].endswith("_kwargs"):
1235
current[field["id"]] = _env_to_dict(field["value"])
1236
elif field["id"].startswith("api_key_"):
1237
current["api_keys"][field["id"]] = field["value"]
@@ -1365,6 +1383,7 @@ def get_default_settings() -> Settings:
1383
browser_model_rl_input=0,
1384
browser_model_rl_output=0,
1385
browser_model_kwargs={"temperature": "0"},
1386
+ browser_http_headers={},
1387
memory_recall_enabled=True,
1388
memory_recall_delayed=False,
1389
memory_recall_interval=3,
@@ -1538,6 +1557,45 @@ def _dict_to_env(data_dict):
1557
return "\n".join(lines)
1558
1559
1560
+def _validate_http_headers(headers: dict[str, str]) -> dict[str, str]:
1561
+ """Validate and sanitize HTTP headers for browser requests"""
1562
+ valid_headers = {}
1563
+
1564
+ # Headers that should not be set manually as they're controlled by the browser
1565
+ dangerous_headers = {
1566
+ 'host', 'content-length', 'connection', 'upgrade', 'expect',
1567
+ 'transfer-encoding', 'te', 'trailer', 'proxy-connection'
1568
+ }
1569
+
1570
+ for key, value in headers.items():
1571
+ # Remove any leading/trailing whitespace
1572
+ key = key.strip()
1573
+ value = value.strip()
1574
+
1575
+ # Skip empty keys or values
1576
+ if not key or not value:
1577
+ continue
1578
+
1579
+ # Check for dangerous headers
1580
+ if key.lower() in dangerous_headers:
1581
+ PrintStyle().warning(f"Skipping potentially dangerous header: {key}")
1582
+ continue
1583
+
1584
+ # Basic header name validation (RFC 7230)
1585
+ if not re.match(r'^[!#$%&\'*+\-.0-9A-Z^_`a-z|~]+$', key):
1586
+ PrintStyle().warning(f"Invalid header name format: {key}")
1587
+ continue
1588
+
1589
+ # Header value validation - remove control characters except tab
1590
+ cleaned_value = re.sub(r'[\x00-\x08\x0A-\x1F\x7F]', '', value)
1591
+ if cleaned_value != value:
1592
+ PrintStyle().warning(f"Cleaned invalid characters from header value: {key}")
1593
+
1594
+ valid_headers[key] = cleaned_value
1595
+
1596
+ return valid_headers
1597
+
1598
+
1599
def set_root_password(password: str):
1600
if not runtime.is_dockerized():
1601
raise Exception("root password can only be set in dockerized environments")
python/tools/browser_agent.py
+13
-1
@@ -39,7 +39,18 @@ class State:
39
40
# for some reason we need to provide exact path to headless shell, otherwise it looks for headed browser
41
pw_binary = ensure_playwright_binary()
42
-
42
+
43
+ # Prepare HTTP headers with error handling
44
+ try:
45
+ http_headers = self.agent.config.browser_http_headers or {}
46
+ if http_headers:
47
+ PrintStyle().info(f"Using HTTP headers: {list(http_headers.keys())}")
48
+ else:
49
+ PrintStyle().info("No custom HTTP headers configured")
50
+ except Exception as e:
51
+ PrintStyle().warning(f"Error processing HTTP headers, using defaults: {e}")
52
+ http_headers = {}
53
+
54
self.browser_session = browser_use.BrowserSession(
55
browser_profile=browser_use.BrowserProfile(
56
headless=True,
@@ -64,6 +75,7 @@ class State:
75
/ "profiles"
76
/ f"agent_{self.agent.context.id}"
77
),
78
+ extra_http_headers=http_headers,
79
)
80
)
81