今天阅读 《整一个同时用于浏览器和 Node.js 的模块》 这篇博文时,看到其中关于浏览器 fetch api 获取请求头部分,心有所感,于是就有了本文。

本文将说一说浏览器 fetch api 与 Forbidden header 的那些事情,在浏览器中可以设置并获取 Forbidden header 吗?

一切开始之前,先看一看官方文档是怎么说的。

For security reasons, some headers can only be controlled by the user agent. These headers include the forbidden header names and forbidden response header names.

出于安全考虑,某些头只能由用户代理控制。这些头信息包括 forbidden header names 和 forbidden response header names。

Warning: Browsers block frontend JavaScript code from accessing the Set-Cookie header, as required by the Fetch spec, which defines Set-Cookie as a forbidden response-header name that must be filtered out from any response exposed to frontend code.

警告: 根据 Fetch 规范,Set-Cookie 是一个禁止的响应标头,对应的响应在被暴露给前端代码前,必须滤除这一响应标头,即浏览器会阻止前端 JavaScript 代码访问 Set-Cookie 标头。


文档告诉我们,在浏览器环境中,向请求设置 Forbidden header ,获取响应的 Forbidden response headerSet-Cookie) 是无法做到的。

真的是这样吗?我不信,我要自己试一试。

(async () => {
    try {
        const resp = await fetch("https://httpbin.org/get", {
        method: "GET",
        headers: {
            Cookie: "test1234",
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "none",
            Test: "test1234",
        },
        });
        const resp_body = await resp.json();
        console.log("https://httpbin.org/get");
        console.log(resp);
        console.log([...resp.headers.entries()]);
        console.log([...resp.headers.entries()].map((h) => h.join(": ")).join("\n"));
        console.log(resp_body);
    } catch (error) {
        console.error(error);
    }

    try {
        const resp = await fetch("https://www.baidu.com/", {
        method: "GET",
        });
        const resp_body = await resp.text();
        console.log("https://www.baidu.com/");
        console.log(resp);
        console.log([...resp.headers.entries()]);
        console.log([...resp.headers.entries()].map((h) => h.join(": ")).join("\n"));
        console.log(resp_body);
    } catch (error) {
        console.error(error);
    }

    try {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "https://httpbin.org/get");

        xhr.setRequestHeader("Cookie", "test1234");
        xhr.setRequestHeader("Sec-Fetch-Dest", "document");
        xhr.setRequestHeader("Sec-Fetch-Mode", "navigate");
        xhr.setRequestHeader("Sec-Fetch-Site", "none");
        xhr.setRequestHeader("Test", "test1234");

        xhr.responseType = "json";

        xhr.addEventListener("load", (ev) => {
        console.log("https://httpbin.org/get");
        console.log(ev);
        console.log(xhr);
        console.log(xhr.response);
        console.log(xhr.getAllResponseHeaders());
        });
        xhr.send();
    } catch (error) {
        console.error(error);
    }

    try {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "https://www.baidu.com/");

        xhr.responseType = "text";

        xhr.addEventListener("load", (ev) => {
        console.log("https://www.baidu.com/");
        console.log(ev);
        console.log(xhr);
        console.log(xhr.response);
        console.log(xhr.getAllResponseHeaders());
        });
        xhr.send();
    } catch (error) {
        console.error(error);
    }
})();

https://www.baidu.com/ 页面,运行上述代码,结果如下:

/images/2023/browser-fetch-forbidden-header/request.png

Forbidden header

/images/2023/browser-fetch-forbidden-header/response.png

Forbidden response header

看来文档写的一点没错,确实无法设置与获取 Forbidden header 。

当然如果仅仅是这样,还不至于让我水一篇博文。

现在是见证奇迹的时刻了。


打开 Violentmonkey API 文档,在 GM_xmlhttpRequest 一节中,有这样的内容。

/images/2023/browser-fetch-forbidden-header/GM_xmlhttpRequest.png

GM_xmlhttpRequest

Some special headers are also allowed:

- 'Cookie'
- 'Host'
- 'Origin'
- 'Referer'
- 'User-Agent'

如果我没有记错的话,这些都是 Forbidden header 吧。怎么 GM_xmlhttpRequest 就可以设置了?我不信。

// ==UserScript==
// @name        GM_xmlhttpRequest Forbidden header Test
// @namespace   bgme.me
// @match       https://example.org/
// @grant       GM_xmlhttpRequest
// @version     1.0
// @author      bgme
// @connect     *
// @connect     httpbin.org
// @connect     www.baidu.com
// @description GM_xmlhttpRequest Forbidden header Test
// ==/UserScript==

try {
    const request = GM_xmlhttpRequest({
        url: "https://httpbin.org/get",
        method: "GET",
        headers: {
        Cookie: "test1234",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
        Test: "test1234",
        },
        responseType: "json",
        onload(ev) {
        console.log("https://httpbin.org/get");
        console.log(ev);
        console.log(ev.response);
        console.log(ev.responseHeaders);

        console.log(request);
        },
    });
} catch (error) {
    console.error(error);
}

try {
    const request = GM_xmlhttpRequest({
        url: "https://www.baidu.com/",
        method: "GET",
        responseType: "text",
        onload(ev) {
        console.log("https://www.baidu.com/");
        console.log(ev);
        console.log(ev.response);
        console.log(ev.responseHeaders);

        console.log(request);
        },
    });
} catch (error) {
    console.error(error);
}

打开 https://example.org/ ,F12 打开 console。

/images/2023/browser-fetch-forbidden-header/GM_xmlhttpRequest_test_Violentmonkey.png

GM_xmlhttpRequest Forbidden header Test on Violentmonkey

/images/2023/browser-fetch-forbidden-header/GM_xmlhttpRequest_test_Tampermonkey.png

GM_xmlhttpRequest Forbidden header Test on Tampermonkey

虽然不愿意相信,但事实摆在面前, GM_xmlhttpRequest 确实突破了浏览器的 Forbidden header 限制。

那么问题又来了,Violentmonkey、Tampermonkey 是怎么做到的?

莫非是 Background scripts 里的 fetch API 有特殊的权限,可以像 Node 那样不受 Forbidden header 限制?

将第一部分创建为 Background scripts,加载插件,打开插件调试 Console 。

/images/2023/browser-fetch-forbidden-header/Background_scripts_fetch.png

fetch on Background scripts

结果如图,很明显,Background scripts 中的 fetch API 也要受到 Forbidden header 的限制。

翻看了一下 Violentmonkey 的源码(requests.jsrequests-core.js),可以看出 Violentmonkey 通过 webRequest API 突破了 Forbidden header 的限制。

但有一个不幸的消息是 webRequest API 在 Manifest V3 中被 Google 干掉了,想要修改请求,就只能使用 declarativeNetRequest API 。 到了 Manifest V3 时代, GM_xmlhttpRequest 很可能就无法实现修改、读取 Forbidden header 的功能了。