single-file-cli 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,10 +12,10 @@ jobs:
12
12
  steps:
13
13
  -
14
14
  name: Checkout sources
15
- uses: actions/checkout@v4
15
+ uses: actions/checkout@v7
16
16
  -
17
17
  name: Set up Node.js
18
- uses: actions/setup-node@v4
18
+ uses: actions/setup-node@v7
19
19
  with:
20
20
  node-version: 24
21
21
  cache: npm
@@ -22,7 +22,7 @@ jobs:
22
22
  steps:
23
23
  # only used to resolve the tag to a commit, so it runs no code from the repository and keeps no
24
24
  # git credentials behind
25
- - uses: actions/checkout@v5
25
+ - uses: actions/checkout@v7
26
26
  with:
27
27
  ref: ${{ github.event.release.tag_name }}
28
28
  persist-credentials: false
@@ -86,11 +86,11 @@ jobs:
86
86
  needs: check_tests
87
87
  runs-on: ubuntu-latest
88
88
  steps:
89
- - uses: actions/checkout@v5
89
+ - uses: actions/checkout@v7
90
90
  with:
91
91
  ref: ${{ github.event.release.tag_name }}
92
92
  # Setup .npmrc file to publish to npm
93
- - uses: actions/setup-node@v5
93
+ - uses: actions/setup-node@v7
94
94
  with:
95
95
  node-version: '24'
96
96
  registry-url: 'https://registry.npmjs.org'
@@ -121,19 +121,19 @@ jobs:
121
121
  exit 1
122
122
  -
123
123
  name: Set up QEMU
124
- uses: docker/setup-qemu-action@v3
124
+ uses: docker/setup-qemu-action@v4
125
125
  -
126
126
  name: Set up Docker Buildx
127
- uses: docker/setup-buildx-action@v3
127
+ uses: docker/setup-buildx-action@v4
128
128
  -
129
129
  name: Login to Docker Hub
130
- uses: docker/login-action@v3
130
+ uses: docker/login-action@v4
131
131
  with:
132
132
  username: ${{ secrets.DOCKER_USERNAME }}
133
133
  password: ${{ secrets.DOCKER_PASSWORD }}
134
134
  -
135
135
  name: Build and push
136
- uses: docker/build-push-action@v6
136
+ uses: docker/build-push-action@v7
137
137
  with:
138
138
  push: true
139
139
  build-args: |
package/build.sh CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  set -e
4
4
 
5
- CORE_PACKAGE="npm:single-file-core@1.5.109"
5
+ CORE_PACKAGE="npm:single-file-core@1.5.115"
6
6
  ESBUILD_PACKAGE="npm:esbuild@0.27.7"
7
7
 
8
8
  project_dir=$(pwd)
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "SingleFile CLI",
5
5
  "exports": {
6
6
  ".": "./single-file-cli-api.js"
package/lib/browser.js CHANGED
@@ -62,7 +62,7 @@ const PROFILE_INCOMPATIBLE_ARGS = [
62
62
  const { build, makeTempDir, readDir, copyFile, mkdir, Command, errors, remove } = Deno;
63
63
  const { join } = path;
64
64
  let child, profilePath, childExited, keepProfilePath, browserDebugPort;
65
- export { launchBrowser, createBrowserProfile, getBrowserOptions, copyProfile, closeBrowser, browserExited };
65
+ export { launchBrowser, createBrowserProfile, getBrowserOptions, copyProfile, pruneProfile, closeBrowser, browserExited };
66
66
 
67
67
  function getBrowserOptions(options) {
68
68
  return {
@@ -71,7 +71,6 @@ function getBrowserOptions(options) {
71
71
  executablePath: options.browserExecutablePath,
72
72
  debug: options.browserDebug,
73
73
  singleProcess: options.browserSingleProcess,
74
- disableWebSecurity: options.browserDisableWebSecurity,
75
74
  width: options.browserWidth,
76
75
  height: options.browserHeight,
77
76
  userAgent: options.userAgent,
@@ -241,6 +240,7 @@ async function closeBrowser() {
241
240
  if (closedProfilePath !== undefined) {
242
241
  if (keepProfilePath) {
243
242
  await removeProfileLockEntries(closedProfilePath).catch(() => { });
243
+ await pruneProfile(closedProfilePath).catch(() => { });
244
244
  } else {
245
245
  try {
246
246
  await remove(closedProfilePath, { recursive: true });
@@ -298,6 +298,27 @@ async function copyProfileEntries(entries, sourcePath, destinationPath) {
298
298
  }
299
299
  }
300
300
 
301
+ // the caches the browser fills during the session are the bulk of the profile and
302
+ // none of them is ever copied to the capture profile, so they are dropped rather
303
+ // than kept and copied over at every run
304
+ async function pruneProfile(profileDirectory) {
305
+ await pruneProfileEntries(await readDir(profileDirectory), profileDirectory);
306
+ }
307
+
308
+ async function pruneProfileEntries(entries, profileDirectory) {
309
+ for (const entry of entries) {
310
+ if (entry.isSymlink) {
311
+ continue;
312
+ }
313
+ const entryPath = join(profileDirectory, entry.name);
314
+ if (PROFILE_IGNORED_ENTRY_NAMES.includes(entry.name)) {
315
+ await remove(entryPath, { recursive: true }).catch(() => { });
316
+ } else if (entry.isDirectory) {
317
+ await pruneProfileEntries(await readDir(entryPath), entryPath);
318
+ }
319
+ }
320
+ }
321
+
301
322
  async function removeProfileLockEntries(profileDirectory) {
302
323
  const entries = await readDir(profileDirectory);
303
324
  for (const entry of entries) {
package/lib/cdp-client.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  } from "./browser.js";
32
32
  import {
33
33
  CDP,
34
- options
34
+ options as cdpOptions
35
35
  } from "simple-cdp";
36
36
  import {
37
37
  FETCH_FUNCTION_NAME,
@@ -62,6 +62,11 @@ const BINDING_CALLED_EVENT_TYPE = "bindingCalled";
62
62
  const LOCALHOST = "http://localhost:";
63
63
  const HEADLESS_USER_AGENT_TOKEN = "Headless";
64
64
  const BROWSER_EXITED_MAX_DELAY = 2000;
65
+ const CORS_SAFELISTED_HEADER_NAMES = ["accept", "accept-language", "content-language"];
66
+ const CORS_UNSAFE_HEADER_VALUE = /[^\t\x20-\x7e]|[():<>?@[\\\]{}]/;
67
+ const CORS_SAFELISTED_HEADER_VALUE_MAX_LENGTH = 128;
68
+ const SERVICE_WORKER_TARGET_TYPE = "service_worker";
69
+ const USER_AGENT_HEADER_NAME = "user-agent";
65
70
 
66
71
  let browserOptions, relaunchBrowserPromise;
67
72
 
@@ -73,10 +78,10 @@ export {
73
78
 
74
79
  async function initialize(singleFileOptions) {
75
80
  if (singleFileOptions.browserServer) {
76
- options.apiUrl = singleFileOptions.browserServer;
81
+ cdpOptions.apiUrl = singleFileOptions.browserServer;
77
82
  } else {
78
83
  browserOptions = getBrowserOptions(singleFileOptions);
79
- options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
84
+ cdpOptions.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
80
85
  }
81
86
  }
82
87
 
@@ -86,7 +91,7 @@ function relaunchBrowser() {
86
91
  relaunchBrowserPromise = (async () => {
87
92
  await closeBrowser();
88
93
  browserOptions.singleProcess = false;
89
- options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
94
+ cdpOptions.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
90
95
  })();
91
96
  }
92
97
  return relaunchBrowserPromise;
@@ -97,7 +102,7 @@ async function getPageData(options) {
97
102
  // compiled here so that an invalid pattern is reported before the page is
98
103
  // loaded, instead of throwing for every request that is intercepted
99
104
  const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
100
- const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, blockedURLPatterns, fetchAbortController: new AbortController() };
105
+ const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, browserInfo: {}, blockedURLPatterns, fetchAbortController: new AbortController() };
101
106
  let targetInfo, cdp;
102
107
  try {
103
108
  logData(["Loading page", EMPTY_PAGE_URL], pageContext);
@@ -107,7 +112,8 @@ async function getPageData(options) {
107
112
  await setupBrowserWindow(cdp, targetInfo.id, pageContext);
108
113
  await setupSecurity(cdp, pageContext);
109
114
  await setupDeviceEmulation(cdp, pageContext);
110
- await setupNetworkInterception(cdp, pageContext);
115
+ await setupNetwork(cdp, pageContext);
116
+ await setupDownloadBehavior(cdp);
111
117
  await setupScriptInjection(cdp, pageContext);
112
118
  const contextId = await getContextId(cdp, pageContext);
113
119
  const pageDataPromise = setupPageDataCapture(cdp, pageContext);
@@ -207,13 +213,18 @@ async function setupSecurity({ Security }, { options, debugMessages }, sessionId
207
213
  }
208
214
  }
209
215
 
210
- async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages }) {
216
+ async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages, browserInfo }) {
211
217
  const needsDeviceMetrics = options.browserMobileEmulation || options.browserDeviceWidth ||
212
218
  options.browserDeviceHeight || options.browserDeviceScaleFactor;
213
219
  if (needsDeviceMetrics) {
214
220
  await setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages });
215
221
  }
216
- await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
222
+ if (options.emulateMediaFeatures) {
223
+ await setupMediaFeatures({ Emulation }, { options, debugMessages });
224
+ }
225
+ // kept so that the resources fetched outside the browser present the same
226
+ // identity as the browser itself
227
+ browserInfo.userAgent = await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
217
228
  }
218
229
 
219
230
  function needsUserAgentOverride(options) {
@@ -257,29 +268,30 @@ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }
257
268
  logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
258
269
  await Emulation.setUserAgentOverride(agentOptions, sessionId);
259
270
  }
271
+ return agentOptions.userAgent;
260
272
  }
261
273
 
262
274
  function removeHeadlessToken(userAgent) {
263
275
  return userAgent.replace(HEADLESS_USER_AGENT_TOKEN, "");
264
276
  }
265
277
 
266
- async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
267
- const DENY_BEHAVIOR = "deny";
278
+ async function setupNetwork({ Fetch, Network }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
268
279
  const { handleAuthRequests, patterns } = getInterceptionOptions(options);
269
280
  await Fetch.enable({ handleAuthRequests, patterns });
270
281
  if (handleAuthRequests) {
271
282
  setupProxyAuth({ Fetch }, { options, debugMessages });
272
283
  }
273
284
  setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns });
274
- if (options.httpHeaders) {
285
+ if (options.httpHeaders && !hasUnsafeHttpHeaders(options)) {
275
286
  await setupHttpHeaders({ Network }, { options, debugMessages });
276
287
  }
277
- if (options.emulateMediaFeatures) {
278
- await setupMediaFeatures({ Emulation }, { options, debugMessages });
279
- }
280
288
  if (options.browserCookies && options.browserCookies.length) {
281
289
  await setupCookies({ Network }, { options, debugMessages });
282
290
  }
291
+ }
292
+
293
+ async function setupDownloadBehavior({ Browser }) {
294
+ const DENY_BEHAVIOR = "deny";
283
295
  await Browser.setDownloadBehavior({ behavior: DENY_BEHAVIOR });
284
296
  }
285
297
 
@@ -287,12 +299,33 @@ function getInterceptionOptions(options) {
287
299
  const REQUEST_STAGE = "Request";
288
300
  const RESPONSE_STAGE = "Response";
289
301
  const handleAuthRequests = Boolean(options.httpProxyUsername);
290
- const patterns = handleAuthRequests ?
302
+ // a request paused on the response stage has already been sent and answered,
303
+ // so a blocked URL is only left uncontacted when the request stage is asked for
304
+ const patterns = handleAuthRequests || hasUnsafeHttpHeaders(options) || hasBlockedURLPatterns(options) ?
291
305
  [{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
292
306
  [{ requestStage: RESPONSE_STAGE }];
293
307
  return { handleAuthRequests, patterns };
294
308
  }
295
309
 
310
+ function hasBlockedURLPatterns(options) {
311
+ return Boolean(options.blockedURLPatterns && options.blockedURLPatterns.length);
312
+ }
313
+
314
+ // headers set on the network layer are part of the CORS decision: one that is not
315
+ // safelisted makes every cross-origin fetch of the page send a preflight, and the
316
+ // servers that do not answer preflights lose the resource to the backend fetch.
317
+ // Such headers are injected on the paused request instead, below that decision
318
+ function hasUnsafeHttpHeaders(options) {
319
+ return Boolean(options.httpHeaders) &&
320
+ Object.entries(options.httpHeaders).some(([name, value]) => !isSafelistedHeader(name, value));
321
+ }
322
+
323
+ function isSafelistedHeader(name, value) {
324
+ return CORS_SAFELISTED_HEADER_NAMES.includes(name.toLowerCase()) &&
325
+ value.length <= CORS_SAFELISTED_HEADER_VALUE_MAX_LENGTH &&
326
+ !CORS_UNSAFE_HEADER_VALUE.test(value);
327
+ }
328
+
296
329
  function setupProxyAuth({ Fetch }, { options, debugMessages }) {
297
330
  const AUTH_REQUIRED_EVENT_TYPE = "authRequired";
298
331
  const PROVIDE_CREDENTIALS_RESPONSE = "ProvideCredentials";
@@ -313,8 +346,12 @@ function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo,
313
346
  const REQUEST_PAUSED_EVENT_TYPE = "requestPaused";
314
347
  const ABORTED_ERROR_REASON = "Aborted";
315
348
  const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
349
+ const injectedHeaders = hasUnsafeHttpHeaders(options) ? options.httpHeaders : undefined;
316
350
  Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
317
- const { requestId, request } = params;
351
+ const { requestId, request, responseStatusCode, responseErrorReason } = params;
352
+ // the request pauses twice when both stages are intercepted, and the
353
+ // headers can only be replaced on the first pause
354
+ const requestStage = responseStatusCode === undefined && responseErrorReason === undefined;
318
355
  // the request is always resumed below, otherwise the page waits for it
319
356
  // until the load timeout expires
320
357
  let blocked = false;
@@ -335,7 +372,8 @@ function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo,
335
372
  }
336
373
  }
337
374
  try {
338
- await Fetch.continueRequest({ requestId }, sessionId);
375
+ const headers = requestStage && injectedHeaders ? getMergedHeaders(request.headers, injectedHeaders) : undefined;
376
+ await Fetch.continueRequest(headers ? { requestId, headers } : { requestId }, sessionId);
339
377
  } catch {
340
378
  // ignored
341
379
  }
@@ -351,6 +389,15 @@ function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo,
351
389
  }
352
390
  }
353
391
 
392
+ // continueRequest replaces the headers of the request instead of adding to them,
393
+ // so the ones the browser prepared are kept and the extra ones merged into them
394
+ function getMergedHeaders(requestHeaders, extraHeaders) {
395
+ const headers = new Map();
396
+ Object.entries(requestHeaders).forEach(([name, value]) => headers.set(name.toLowerCase(), { name, value }));
397
+ Object.entries(extraHeaders).forEach(([name, value]) => headers.set(name.toLowerCase(), { name, value }));
398
+ return Array.from(headers.values());
399
+ }
400
+
354
401
  function captureHttpInfo(params, urlState, { options, debugMessages, httpInfo }) {
355
402
  const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
356
403
  const DOCUMENT_RESOURCE_TYPE = "Document";
@@ -424,8 +471,9 @@ async function setupScriptInjection(cdp, { options, debugMessages }) {
424
471
  await setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages });
425
472
  }
426
473
 
427
- // out-of-process frames are separate targets, so the scripts registered above
428
- // do not reach them; they are attached paused, injected and resumed instead
474
+ // out-of-process frames and service workers are separate targets, so neither the
475
+ // scripts registered above nor the network options reach them; they are attached
476
+ // paused, set up and resumed instead
429
477
  async function setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages }) {
430
478
  const { Page, Runtime, Target } = cdp;
431
479
  const ATTACHED_TO_TARGET_EVENT_TYPE = "attachedToTarget";
@@ -434,7 +482,13 @@ async function setupFrameScriptInjection(cdp, scriptSource, { options, debugMess
434
482
  Target.addEventListener(ATTACHED_TO_TARGET_EVENT_TYPE, ignoringErrors(async ({ params }) => {
435
483
  const { sessionId, targetInfo, waitingForDebugger } = params;
436
484
  try {
437
- if (targetInfo.type === IFRAME_TARGET_TYPE) {
485
+ if (targetInfo.type === SERVICE_WORKER_TARGET_TYPE) {
486
+ logData(["Applying network options to service worker", targetInfo.url], { options, debugMessages });
487
+ // the requests a worker makes on behalf of the page are answered
488
+ // for by its own target, and it is attached paused, so the options
489
+ // are in place before it can serve a single one
490
+ await setupNetworkSession(cdp, sessionId, { options, debugMessages });
491
+ } else if (targetInfo.type === IFRAME_TARGET_TYPE) {
438
492
  logData(["Injecting scripts into frame", targetInfo.url], { options, debugMessages });
439
493
  // the scripts registered below only run when the Page domain is
440
494
  // enabled on the session
@@ -463,17 +517,21 @@ async function setupFrameScriptInjection(cdp, scriptSource, { options, debugMess
463
517
  await Target.setAutoAttach(autoAttachOptions);
464
518
  }
465
519
 
466
- async function setupFrameSession({ Browser, Emulation, Fetch, Network, Security }, sessionId, { options, debugMessages }) {
520
+ async function setupNetworkSession({ Fetch, Network }, sessionId, { options, debugMessages }) {
467
521
  const { handleAuthRequests, patterns } = getInterceptionOptions(options);
468
- if (handleAuthRequests || (options.blockedURLPatterns && options.blockedURLPatterns.length)) {
522
+ if (handleAuthRequests || hasUnsafeHttpHeaders(options) || hasBlockedURLPatterns(options)) {
469
523
  // the requestPaused and authRequired listeners are registered once on
470
524
  // the connection and receive the events of every session
471
525
  await Fetch.enable({ handleAuthRequests, patterns }, sessionId);
472
526
  }
473
- await setupSecurity({ Security }, { options, debugMessages }, sessionId);
474
- if (options.httpHeaders) {
527
+ if (options.httpHeaders && !hasUnsafeHttpHeaders(options)) {
475
528
  await setupHttpHeaders({ Network }, { options, debugMessages }, sessionId);
476
529
  }
530
+ }
531
+
532
+ async function setupFrameSession({ Browser, Emulation, Fetch, Network, Security }, sessionId, { options, debugMessages }) {
533
+ await setupNetworkSession({ Fetch, Network }, sessionId, { options, debugMessages });
534
+ await setupSecurity({ Security }, { options, debugMessages }, sessionId);
477
535
  if (options.emulateMediaFeatures) {
478
536
  await setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId);
479
537
  }
@@ -643,7 +701,7 @@ function setupPageDataCapture({ Runtime }, { options, debugMessages }) {
643
701
  });
644
702
  }
645
703
 
646
- async function setupBindings({ Page, Runtime }, { options, debugMessages, blockedURLPatterns, fetchAbortController }) {
704
+ async function setupBindings({ Page, Runtime }, { options, debugMessages, browserInfo, blockedURLPatterns, fetchAbortController }) {
647
705
  await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
648
706
  if (options.embedScreenshot && options.compressContent) {
649
707
  await setupScreenshotCapture({ Page, Runtime }, { options, debugMessages });
@@ -654,7 +712,7 @@ async function setupBindings({ Page, Runtime }, { options, debugMessages, blocke
654
712
  await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
655
713
  Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
656
714
  if (params.name === FETCH_FUNCTION_NAME) {
657
- await handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId);
715
+ await handleFetchRequest({ Runtime }, params, { options, debugMessages, browserInfo, blockedURLPatterns, fetchAbortController }, sessionId);
658
716
  }
659
717
  }, { options, debugMessages }));
660
718
  }
@@ -717,7 +775,7 @@ async function setupPdfCapture({ Page, Runtime }, { options, debugMessages }) {
717
775
  }
718
776
  }
719
777
 
720
- async function handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId) {
778
+ async function handleFetchRequest({ Runtime }, params, { options, debugMessages, browserInfo, blockedURLPatterns, fetchAbortController }, sessionId) {
721
779
  const BLOCKED_URL_ERROR_MESSAGE = "Blocked URL";
722
780
  const { executionContextId: contextId, payload } = params;
723
781
  const { requestId, url, options: fetchOptions } = JSON.parse(payload);
@@ -729,9 +787,12 @@ async function handleFetchRequest({ Runtime }, params, { options, debugMessages,
729
787
  logData(["Blocking request", url], { options, debugMessages });
730
788
  throw new Error(BLOCKED_URL_ERROR_MESSAGE);
731
789
  }
790
+ // this fetch runs outside the browser, so without the user agent below it
791
+ // would reach the server under the runtime's own identity while every
792
+ // request the browser made presented another one
732
793
  const headers = Object.assign({}, fetchOptions.headers, options.httpHeaders);
733
- if (options.userAgent) {
734
- headers["user-agent"] = options.userAgent;
794
+ if (browserInfo.userAgent && !Object.keys(headers).some(name => name.toLowerCase() == USER_AGENT_HEADER_NAME)) {
795
+ headers[USER_AGENT_HEADER_NAME] = browserInfo.userAgent;
735
796
  }
736
797
  const response = await fetch(url, Object.assign({}, fetchOptions, { headers, signal: fetchAbortController.signal }));
737
798
  const arrayBuffer = await response.arrayBuffer();
@@ -808,7 +869,7 @@ async function disableCdpDomains({ Console, Fetch, Network, Page, Runtime }, { o
808
869
  await Fetch.disable();
809
870
  await Runtime.disable();
810
871
  await Page.disable();
811
- if (options.httpHeaders) {
872
+ if (options.httpHeaders && !hasUnsafeHttpHeaders(options)) {
812
873
  await Network.disable();
813
874
  }
814
875
  if (options.consoleMessagesFile) {