You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firefox扩展匹配自定义Content-Type渲染HTML时触发下载的问题

问题描述

我正在尝试为Web定义一种全新的文件类型,该类型文件可通过浏览器扩展渲染为HTML。预期实现方案为:服务端返回该自定义类型的文件时,携带HTTP响应头Content-Type: text/mytype;扩展会检查响应头,若Content-Type匹配text/mytype,则拦截响应体,将其替换为渲染完成的HTML,实现安装插件后该文件类型可与Web生态良好交互的效果。

作为概念验证,我开发了一款可拦截响应头并将其渲染到响应内容中的插件,但该插件仅在Content-Type为text/html时可正常运行。如果在服务端将响应的Content-Type修改为text/mytype,Firefox会直接下载该文件,完全没有执行扩展代码。

核心疑问:是否需要监听其他事件,才能提前拦截/修改响应头,让扩展代码正常执行,而非触发浏览器的文件下载行为?

现有实现代码

manifest.json

{
  "description": "Altering HTTP responses",
  "manifest_version": 2,
  "name": "http-response-filter",
  "version": "1.0",
  "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/http-response",
  "icons": {
    "48": "pen.svg"
  },
  "permissions": [
    "webRequest", "webRequestBlocking", "<all_urls>"
  ],
  "background": {
    "scripts": ["background.js"]
  },
  "browser_specific_settings": {
    "gecko": {
      "strict_min_version": "57.0a1"
    }
  }
}

background.js

var contentType;

function contentListener(details) {
  //if(contentType === 'text/mytype') {
    let filter = browser.webRequest.filterResponseData(details.requestId);
    let decoder = new TextDecoder("utf-8");
    let encoder = new TextEncoder();

    filter.ondata = event => {
      let str = decoder.decode(event.data, {stream: true});

      // Later I want to replace this line with code that parses the
      // document and renders it to HTML
      str = str.replace(/Example/g, 'Content-Type: ' + contentType);

      filter.write(encoder.encode(str));
      filter.disconnect();
    }
  //}

  return {};
}

browser.webRequest.onBeforeRequest.addListener(
  contentListener,
  {urls: ["<all_urls>"], types: ["main_frame"]},
  ["blocking"]
);

function headersListener(details) {
  let headers = details.responseHeaders;
  for(var i = 0; i < headers.length; i++) {
    if(headers[i].name.toLowerCase() === 'content-type') {
      contentType = headers[i].value;

      // Try setting the content type to HTML to prevent downloading--
      // doesn't work
      headers[i].value = 'text/html; charset=utf-8';
    }
  }

browser.webRequest.onBeforeRequest.addListener(
  contentListener,
  {urls: ["<all_urls>"], types: ["main_frame"]},
  ["blocking"]
);
}

browser.webRequest.onHeadersReceived.addListener(
  headersListener, {
    urls: ["<all_urls>"],
    types: ["main_frame"]
  }, ["blocking", "responseHeaders"]
);
问题原因

现有代码不生效有两个核心问题:

  1. onHeadersReceived监听器修改完响应头后,没有将修改后的头通过返回值传回浏览器,浏览器始终拿不到修改后的text/html类型,会按照原始的text/mytype触发文件下载逻辑,根本不会进入页面加载流程,扩展的响应拦截逻辑自然不会执行。
  2. 全局变量存储contentType、重复注册onBeforeRequest监听器的写法存在竞态问题,多请求并发时会出现类型匹配错误,且提前在onBeforeRequest阶段初始化响应过滤器,会导致非目标类型的请求也被拦截,逻辑冗余。
修复方案

不需要额外监听其他事件,修复现有逻辑即可:

  • 在onHeadersReceived监听器的执行末尾,返回包含修改后响应头的对象,确保浏览器能收到修改后的Content-Type,跳过下载逻辑。
  • 移除重复注册的onBeforeRequest监听器,改用Map将请求ID和对应的Content-Type绑定存储,避免全局变量的竞态问题。
  • 仅在匹配到自定义text/mytype类型时,才为对应请求绑定响应流过滤器,减少不必要的性能消耗。

修复后的background.js代码如下:

// 按requestId存储对应请求的Content-Type,避免全局变量竞态
const requestContentTypeMap = new Map();

function headersListener(details) {
  const headers = details.responseHeaders;
  let isTargetType = false;
  for(let i = 0; i < headers.length; i++) {
    if(headers[i].name.toLowerCase() === 'content-type') {
      const originalType = headers[i].value;
      requestContentTypeMap.set(details.requestId, originalType);
      // 匹配到自定义类型时修改Content-Type,阻止浏览器触发下载
      if(originalType.includes('text/mytype')) {
        isTargetType = true;
        headers[i].value = 'text/html; charset=utf-8';
      }
    }
  }

  // 仅对目标请求绑定响应过滤逻辑
  if(isTargetType) {
    bindResponseFilter(details.requestId);
  }
  // 关键:必须返回修改后的响应头,否则所有头修改都不生效
  return {responseHeaders: headers};
}

function bindResponseFilter(requestId) {
  const filter = browser.webRequest.filterResponseData(requestId);
  const decoder = new TextDecoder("utf-8");
  const encoder = new TextEncoder();
  const originalContentType = requestContentTypeMap.get(requestId);

  filter.ondata = event => {
    let contentStr = decoder.decode(event.data, {stream: true});
    // 此处可替换为自定义格式转HTML的渲染逻辑
    contentStr = contentStr.replace(/Example/g, 'Content-Type: ' + originalContentType);
    filter.write(encoder.encode(contentStr));
  };

  filter.onstop = () => {
    filter.close();
    // 请求结束后清理缓存,避免内存泄漏
    requestContentTypeMap.delete(requestId);
  };
}

// 仅注册一次响应头监听
browser.webRequest.onHeadersReceived.addListener(
  headersListener,
  {
    urls: ["<all_urls>"],
    types: ["main_frame"]
  },
  ["blocking", "responseHeaders"]
);

修改后,浏览器在收到响应头阶段就会识别到Content-Type为text/html,不会触发下载,会正常进入页面加载流程,扩展可以正常拦截响应流,将自定义格式内容渲染为HTML。


内容的提问来源于stack exchange,提问作者kerkeslager

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 17:51:23