generated at
GM_fetch

whatwg-fetch polyfillをベースに作るtakker

2024-06-06
11:59:46 禁止ヘッダーを入れ込むために、Headersを使う
11:02:50 Origin (HTTP header)に対応した
2023-03-19
19:36:19 Referer HeaderRequestのconstructor及びRequest.headersに指定できなかったのを直した
RequestInit.referrerを別ルートでGM_xmlhttpRequestに渡すようにした
2023-02-23
16:30:59 Requestの本体が渡されていなかった
await request.text() するか、 init?.body から取り出すしかない
2023-02-06
10:23:43 Response.urlObject.defineProperty()経由でないと設定できない
09:59:12 readystatechange eventでHEADER_RECEIVEDになったときにresolveするように変えた
ReadableStreamで進捗状況を取得できるようになった
fetchもこのstateでresolveするらしいので、こちらのほうがよりfetchに近い
> promise は、サーバがヘッダを応答するとすぐに組み込みの Response クラスのオブジェクトで resolve します。
規格で決まっている挙動かは不明
MDNで同様の説明は見つからなかった
09:28:52 書き換え終了
変更するところはほとんどなかった
2023-02-03 09:37:54 もうあったんかい!
しかもtakkerと同じくwhatwg-fetch polyfillからのforkだったようだ
まあ、書き方が古いのでどのみち書き直すことになっただろうが
takkerが考慮していないケースにも対応しているみたいなので、参考にする

user.js
import { GM_fetch } from "./mod.ts"; unsafeWindow.GM_fetch = GM_fetch;

$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/GM_fetch/mod.ts
mod.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> /// <reference lib="dom.iterable" /> import "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/3b80d99d9f86950c913035044105257b9fd6f3a9/types/tampermonkey/index.d.ts"; const parseHeaders = (rawHeaders: string) => new Headers( rawHeaders // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space // https://tools.ietf.org/html/rfc7230#section-3.2 .replace(/\r?\n[\t ]+/g, " ") .split(/\r\n|\r|\n/) .flatMap((header) => { const [key, value] = header.split(":").map((text) => text.trim()); if (!key) return []; return [[key, value]] as [string, string][]; }), ); /** CORSを突破してfetchする * * interfaceは`fetch`と同一 */ export const GM_fetch = ( input: RequestInfo | URL, init?: RequestInit, ): Promise<Response> => new Promise((resolve, reject) => { const headers = Object.fromEntries( new Headers( input instanceof Request ? input.headers : init?.headers, ) .entries(), ); if (input instanceof Request) { headers.Referer = input.referrer; headers["Referrer-Policy"] = input.referrerPolicy; } if (init?.referrer) { headers.Referer = init.referrer; } if (init?.referrerPolicy) { headers["Referrer-Policy"] = init.referrerPolicy; } const request = new Request(input, init); if (request.signal?.aborted) { reject(new DOMException("Aborted", "AbortError")); return; } const { abort } = GM_xmlhttpRequest({ // @ts-ignore 多分変な文字列は入ってこないはず method: request.method, url: request.url, headers, // await request.text() でも同じことができるが、二度手間なのでinitから直接取り出す ...(init?.body ? ({ data: init.body }) : ({})), anonymous: request.credentials === "omit", // @ts-ignore streamも指定可能 responseType: "stream", fetch: true, onerror: () => { reject(new TypeError("Network request failed")); }, ontimeout: () => { reject(new TypeError("Network request timeout")); }, onabort: () => { reject(new DOMException("Aborted", "AbortError")); }, // fetchはHEADERS_RECEIVEDの段階でresolveするので、それにあわせる // https://ja.javascript.info/fetch onreadystatechange: (res) => { switch (res.readyState) { case 2: { const response = new Response(res.response, { status: res.status, statusText: res.statusText, headers: parseHeaders(res.responseHeaders), }); // urlはconstructor経由で設定できない Object.defineProperty(response, "url", { value: res.finalUrl }); resolve(response); break; } case 4: request.signal?.removeEventListener?.("abort", abort); break; default: break; } }, }); request.signal?.addEventListener?.("abort", abort); });

scrapboxで動くサンプル
scrapbox.user.js
// ==UserScript== // @name GM_fetch // @namespace https://scrapbox.io // @version 0.1.13 // @downloadURL https://scrapbox.io/api/code/takker/GM_fetch/scrapbox.user.js // @updateURL https://scrapbox.io/api/code/takker/GM_fetch/scrapbox.user.js // @description An implementation of the fetch API which leverages `GM_xmlhttpRequest` // @author takker // @homepage https://scrapbox.io/takker/GM_fetch // @match https://scrapbox.io/* // @connect * // @grant GM_xmlhttpRequest // @license MIT // @copyright Copyright (c) 2023 takker // ==/UserScript== var u=n=>new Headers(n.replace(/\r?\n[\t ]+/g," ").split(/\r\n|\r|\n/).flatMap(e=>{let[s,o]=e.split(":").map(t=>t.trim());return s?[[s,o]]:[]})),l=(n,e)=>new Promise((s,o)=>{let t=Object.fromEntries(new Headers(n instanceof Request?n.headers:e?.headers).entries());n instanceof Request&&(t.Referer=n.referrer,t["Referrer-Policy"]=n.referrerPolicy),e?.referrer&&(t.Referer=e.referrer),e?.referrerPolicy&&(t["Referrer-Policy"]=e.referrerPolicy);let r=new Request(n,e);if(r.signal?.aborted){o(new DOMException("Aborted","AbortError"));return}let{abort:a}=GM_xmlhttpRequest({method:r.method,url:r.url,headers:t,...e?.body?{data:e.body}:{},anonymous:r.credentials==="omit",responseType:"stream",fetch:!0,onerror:()=>{o(new TypeError("Network request failed"))},ontimeout:()=>{o(new TypeError("Network request timeout"))},onabort:()=>{o(new DOMException("Aborted","AbortError"))},onreadystatechange:i=>{switch(i.readyState){case 2:{let d=new Response(i.response,{status:i.status,statusText:i.statusText,headers:u(i.responseHeaders)});Object.defineProperty(d,"url",{value:i.finalUrl}),s(d);break}case 4:r.signal?.removeEventListener?.("abort",a);break;default:break}}});r.signal?.addEventListener?.("abort",a)});unsafeWindow.GM_fetch=l;

#2024-06-06 11:03:40
#2023-03-28 17:09:18
#2023-03-21 05:42:19
#2023-03-19 19:37:48
#2023-02-23 16:30:55
#2023-02-19 20:55:46
#2023-02-06 10:01:58
#2023-02-03 09:35:40