npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kobzi/gmfetch

v1.4.0

Published

Drop-in fetch() replacement for userscripts, backed by GM_xmlhttpRequest. Bypasses CORS, forbidden headers, and cookie restrictions.

Readme

gmFetch

Drop-in fetch() replacement for userscripts, powered by GM_xmlhttpRequest. Supports cross-origin requests, forbidden headers, cookie injection, proxies, streaming, and upload progress while preserving familiar Fetch API ergonomics.

Available in three variants:

  • Full (~3.3 KB min, ~1.7 KB gzip) — closely aligned with Fetch spec, SRI, streaming, cache modes, GM options
  • Lite (~2.1 KB min, ~1.1 KB gzip) — core fetch semantics, AbortSignal, forbidden headers, throttle-immune gm.timeout + gm.onprogress
  • Micro (~1.0 KB min, ~0.65 KB gzip) — absolute minimum for simple GET/POST, no abort, no timeout
// Full
import gmFetch from "@kobzi/gmfetch";

// Lite
import gmFetch from "@kobzi/gmfetch/lite";

// Micro
import gmFetch from "@kobzi/gmfetch/micro";

// IIFE (classic userscript)
// @require https://cdn.jsdelivr.net/npm/@kobzi/gmfetch@latest/dist/gmFetch.iife.min.js
// @require https://cdn.jsdelivr.net/npm/@kobzi/gmfetch@latest/dist/gmFetch.lite.iife.min.js
// @require https://cdn.jsdelivr.net/npm/@kobzi/gmfetch@latest/dist/gmFetch.micro.iife.min.js
const r = await gmFetch("https://api.example.com/data");
const data = await r.json();

Full vs Lite vs Micro

| Feature | Full | Lite | Micro | |---|:---:|:---:|:---:| | Request normalisation (URL, method, body) | ✓ | ✓ | ✓ | | Native Response (ok, json, blob, clone) | ✓ | ✓ | ✓ | | Response url | ✓ | ✓ | ✓ | | credentials → anonymous mapping | ✓ | ✓ | ✓ | | redirect passthrough | ✓ | ✓ | ✓ | | Binary body support | ✓ | ✓ | ✓ | | Text body sent as-is (no forced Blob) | ✓ | ✓ | ✓ | | status:0 → TypeError | ✓ | ✓ | ✓ | | AbortSignal / AbortController | ✓ | ✓ | ✗ | | Forbidden headers preservation | ✓ | ✓ | ✗ | | RFC 7230 header folding | ✓ | ✓ | ✗ | | Response type / redirected | ✓ | ✓ | ✓ | | Set-Cookie access (getSetCookie) | ✓ | ✓ | ✓ | | Error semantics (DOMException types) | ✓ | ✓ | ✗ | | Cache mode mapping | ✓ | ✗ | ✗ | | ReadableStream response | ✓ | ✗ | ✗ | | SRI integrity verification | ✓ | ✗ | ✗ | | GM options (cookie, proxy, fetch, etc.) | ✓ | ⚠️ timeout + onprogress only | ✗ | | Upload/download progress | ✓ | ⚠️ download only | ✗ | | Error .cause with raw GM event | ✓ | ✓ | ✓ | | redirect: "manual" Location preserved | ✓ | ✓ | ✓ | | Mid-download network error → rejection | ✓ | ✓ | ✓ |

Use Micro when: simple GET/POST, grab JSON, size is everything, no abort needed.

Use Lite when: need AbortSignal, forbidden headers (Cookie/UA), proper error handling.

Use Full when: need GM-specific features, SRI, streaming, cache control, progress events.


Installation

The import / @require lines at the top are all you need. Requirements: @grant GM_xmlhttpRequest (or GM.xmlHttpRequest), plus @connect <domain> for cross-origin. The IIFE exposes gmFetch as a global — pin a version (@kobzi/[email protected]) for stability.

Compatibility

| Engine | Support | |---|---| | Tampermonkey 4.x+ | Full. gm.proxy needs 5.5+ (FF). gm.cookiePartition needs 5.2+. | | Violentmonkey 2.13+ | Works. No gm.proxy/gm.cookiePartition/gm.fetch. | | Greasemonkey 4.x | Partial. Uses GM.xmlHttpRequest. No streaming, no redirect/nocache/revalidate/anonymous/cookie/proxy/fetch/maxRedirects. |

Runtime: crypto.subtle (for SRI, full only).


API

function gmFetch(input: RequestInfo | URL, init?: GmFetchInit): Promise<Response>

Signature matches window.fetch(). Full and lite extend init with an optional gm field:

interface GmFetchInit extends RequestInit {
  gm?: GmOptions;                                  // full: everything below
}
interface GmFetchLiteInit extends RequestInit {
  gm?: { timeout?: number; onprogress?: (ev) => void };  // lite: this subset only
}

Standard RequestInit fields

| Field | Full | Lite | Micro | Behaviour | |---|:---:|:---:|:---:|---| | method | ✓ | ✓ | ✓ | As-is. CONNECT/TRACE/TRACK rejected per spec. | | headers | ✓ | ✓ | ✓ | Full/Lite: forbidden headers preserved (plain object/tuples). Micro: normalised via Request only. | | body | ✓ | ✓ | ✓ | Text (string/URLSearchParams) sent as-is; other types buffered as Blob with binary: true. | | credentials | ✓ | ✓ | ✓ | "omit"anonymous: true. Others use GM defaults. | | cache | ✓ | ✗ | ✗ | "no-store"/"reload"nocache. "no-cache"revalidate. "only-if-cached" → rejected. | | redirect | ✓ | ✓ | ✓ | "follow", "error", "manual" passed to GM. | | signal | ✓ | ✓ | ✗ | AbortSignal with reason propagation. Cancels GM request. | | integrity | ✓ | ✗ | ✗ | SRI verification (sha256/384/512). |

GmOptions — the gm field

Only whitelisted keys are forwarded (protects internal callbacks). Full supports all of the below; lite keeps just timeout and onprogress:

| Field | Description | |---|---| | cookie | Patch cookies into request set (additive, not replacing). | | cookiePartition | CHIPS: { topLevelSite: "https://..." }. TM 5.2+. | | fetch | Background fetch via TM service worker (Chrome MV3). | | proxy | { type, host, port, username?, password? }. TM 5.5+, Firefox. | | user / password | HTTP Basic Auth. | | timeout | Ms. Immune to tab throttling. 0 = none. | | maxRedirects | Max redirects to follow. 0 = don't follow. TM 6180+. | | onprogress | Download progress callback. | | onloadstart | Load-start callback. | | onuploadprogress | Upload progress callback. Not available in native fetch. TM 4.x+. | | overrideMimeType | Force response MIME (e.g. "text/html; charset=gbk"). | | context | Arbitrary value attached to GM progress events as event.context (shared-callback dispatch). |


Request body

The body is sent in the format that matches its type, so servers receive what they expect:

| Body type | Sent as | binary | |---|---|:---:| | string (e.g. JSON.stringify(...)) | text, as-is | false | | URLSearchParams | text, as-is | false | | Blob / File | buffered Blob | true | | ArrayBuffer / TypedArray / DataView | buffered Blob | true | | FormData | buffered Blob (multipart) | true |

Text bodies are forwarded directly rather than wrapped in a Blob with binary: true. This matters because some servers reject or mishandle binary blob uploads when they expect a plain JSON/form payload. The Content-Type computed by the Request constructor is always included in the request headers regardless of body type.

// Sent as a normal JSON text body, not a binary blob
await gmFetch("https://api.example.com/items", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "thing" }),
});

Empty bodies are omitted entirely (no empty Blob or empty string is dispatched). Applies to all three variants.

Firefox note: body detection is based on the original init.body, not on Request.body. Some userscript engines (notably Firefox) expose Request.body as null even when a body was supplied; relying on it would silently drop POST payloads. Fixed in 1.3.1.


Headers

Pass as plain object or array of tuples to preserve forbidden headers:

await gmFetch("https://example.com", {
  headers: {
    "Cookie": "session=abc",
    "User-Agent": "Custom/1.0",
    "Referer": "https://other.com",
  },
});

⚠️ new Headers({ Cookie: "x" }) strips forbidden headers at construction. Use plain objects.

Response Set-Cookie

const r = await gmFetch("https://example.com/login", { method: "POST" });
const cookies = r.headers.getSetCookie(); // ["session=abc; HttpOnly", ...]

Note: Set-Cookie availability depends on userscript engine and browser. Tampermonkey exposes it; other engines may vary.

Cookies in one table

Reading response cookies (getSetCookie(), above) works in all variants; sending them needs full/lite (micro's Request constructor strips them):

| Goal | Use | Variant | |---|---|---| | Send exact cookies, ignore browser session | credentials: "omit" + headers: { Cookie: "..." } | full, lite | | Add cookies on top of browser session | gm: { cookie: "..." } (additive) | full | | Browser session as-is | default (nothing) | all |


Timeouts and abort

// AbortSignal (standard) — works in both full and lite.
// ⚠️ Rides page timers, which browsers throttle in background tabs — a "5 s" timeout
// can take a minute+ in an inactive tab.
await gmFetch(url, { signal: AbortSignal.timeout(5000) });

// gm.timeout — enforced by the GM layer, immune to tab throttling (full and lite)
await gmFetch(url, { gm: { timeout: 5000 } });

// Manual abort
const ctrl = new AbortController();
gmFetch(url, { signal: ctrl.signal });
ctrl.abort();

All produce DOMException with name "TimeoutError" or "AbortError" (full and lite only — micro has no abort/timeout support).


Errors

| Cause | Error | |---|---| | GM not granted | DOMException("...", "NotFoundError") (full/lite). Micro throws a plain TypeError. | | Abort / signal | DOMException("...", "AbortError") or signal's reason | | Timeout | DOMException("...", "TimeoutError") | | Network / DNS / @connect | TypeError("Failed to fetch") | | status: 0 | TypeError("Failed to fetch") | | SRI mismatch (full) | TypeError("gmFetch: integrity mismatch") | | only-if-cached (full) | TypeError("gmFetch: only-if-cached unsupported") |

The message is kept spec-generic (native fetch never leaks network failure details). For debugging, network errors (onerror and status: 0) attach the raw GM event on error.cause in all variants, so you can inspect status, statusText, finalUrl, responseHeaders, etc.:

try {
  await gmFetch("https://example.com");
} catch (e) {
  console.error(e.message);        // "Failed to fetch"
  console.error(e.cause?.error);   // GM-provided detail, if any
  console.error(e.cause?.finalUrl);
}

Micro's cause is set on onerror and status: 0; a missing grant still surfaces as a plain TypeError (no NotFoundError DOMException like full/lite).


Examples

POST JSON and forbidden-header examples live in Request body and Headers above.

Progress reporting (full)

// Download progress
await gmFetch("https://example.com/big.zip", {
  gm: {
    onprogress: ({ loaded, total, lengthComputable }) => {
      if (lengthComputable) console.log(`Download: ${(loaded / total * 100).toFixed(1)}%`);
    },
  },
});

// Upload progress — not available in native fetch!
await gmFetch("https://example.com/upload", {
  method: "POST",
  body: largeBlob,
  gm: {
    onuploadprogress: ({ loaded, total }) => {
      console.log(`Upload: ${(loaded / total * 100).toFixed(1)}%`);
    },
  },
});

Streaming (full)

const r = await gmFetch("https://example.com/stream");
const reader = r.body!.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process(value);
}

Proxy (full, Firefox, TM 5.5+)

await gmFetch("https://example.com", {
  gm: { proxy: { type: "socks", host: "127.0.0.1", port: 9050, proxyDNS: true } },
});

SRI integrity (full)

const r = await gmFetch("https://cdn.example.com/lib.js", {
  integrity: "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
});

Everything together (CDN/IIFE)

// ==UserScript==
// @name        Scraper
// @grant       GM_xmlhttpRequest
// @connect     api.example.com
// @require     https://cdn.jsdelivr.net/npm/@kobzi/gmfetch@latest/dist/gmFetch.iife.min.js
// ==/UserScript==

(async () => {
  const r = await gmFetch("https://api.example.com/data", {
    method: "POST",
    headers: { "Cookie": "auth=abc", "User-Agent": "Bot/1.0" },
    body: JSON.stringify({ query: "test" }),
    cache: "no-store",
    gm: { timeout: 15_000 },      // throttle-immune — fires on time even in background tabs
  });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  console.log(await r.json());
})();

Swap the @require for the lite build and drop cache — the rest works unchanged.


Limitations

Silently ignored (no GM equivalent): mode, referrer, referrerPolicy, keepalive, priority, window. Note: the Referer header can still be set manually via headers: { "Referer": "..." } — only the automatic policy fields are ignored.

Not supported (GM_xmlhttpRequest limitation):

  • duplex: "half" — upload streaming is not possible; body is always fully buffered before sending.
  • response.trailer — HTTP trailers are not exposed by GM.
  • Request body streaming — bodies are never streamed. Text bodies (string, URLSearchParams) are passed through as-is; binary bodies (Blob, ArrayBuffer, typed arrays, FormData) are buffered to a Blob before dispatch.

Spec-divergent:

  • redirect: "manual" — returns 3xx with readable Location header (spec says opaque response with status: 0). GM gives more info than spec allows.
  • cache: "force-cache" — falls back to default (no GM equivalent)
  • credentials: "same-origin" — behaves like "include" (GM is privileged)
  • response.type — always "basic" (GM bypasses CORS entirely)
  • response.clone() — works for blob responses; may fail for streaming responses depending on TM/browser implementation details.

Building

npm install
npm run build

Output (dist/): gmFetch[.lite|.micro].esm.min.js + .iife.min.js, plus matching .d.ts type declarations.

Sizes (esbuild + terser, minified — v1.4.0 IIFE, the builds @require uses; ESM is within a few bytes):

| Variant | Raw | Gzip | |---|---:|---:| | Full | 3374 B (3.30 KB) | 1775 B | | Lite | 2108 B (2.06 KB) | 1146 B | | Micro | 1066 B (1.04 KB) | 666 B |

Pipeline: esbuild (bundle + minify, target es2024) → terser (3-pass compress + toplevel mangle).

The build targets ES2024 (modern browsers). If you need to support older environments, fork and change --target in package.json scripts.

Zero runtime dependencies. Dev: esbuild + terser + typescript.


TypeScript

// Full
import gmFetch, {
  type GmFetchInit,
  type GmOptions,
  type GmProxyConfig,
  type GmProgressEvent,
} from "@kobzi/gmfetch";

// Lite
import gmFetch, { type GmFetchLiteInit } from "@kobzi/gmfetch/lite";

// Micro
import gmFetch, { type GmFetchMicroInit } from "@kobzi/gmfetch/micro";

Requires lib: ["ES2024", "DOM"]. For IIFE usage, add a .d.ts with declare function gmFetch(...).


Background

Inspired by @sec-ant/gm-fetch and @trim21/gm-fetch. This library goes further — carefully aligned Fetch semantics, preserved forbidden headers, full GM API surface, SRI integrity, and a lite variant for size-conscious scripts.


Comparison

How gmFetch stacks up against other GM_xmlhttpRequest-based fetch wrappers. In the @kobzi column, the letters say which of our variants ship the feature: F = full · L = lite · M = micro.

| Feature | @kobzi | @sec-ant | @trim21 | gmxhr-fetch | @uwx/gm-fetch | |---|:---:|:---:|:---:|:---:|:---:| | Size (IIFE, min) | 1.0–3.3 KB | 1.9 KB | 2.1 KB | 0.9 KB | 12.4 KB | | AbortSignal + cleanup | F·L | ⚠️ leak | ⚠️ leak | ✗ | ✗ | | signal.reason propagation | F·L | ✗ | ✗ | ✗ | ✗ | | status:0 → TypeError | F·L·M | ✗ | ✗ | ✗ | ✗ | | Forbidden request headers | F·L | ✗ | ✗ | ✗ | ✗ | | Set-Cookie access | F·L·M | ✓ | ✗ | ✗ | ✗ | | Binary body (no corruption) | F·L·M | ✓ | ✗ | ✗ | ✓ | | Response url/type/redirected | F·L·M | ✓ | ⚠️ inverted | ✗ | ✓ | | Cache mode mapping | F | ⚠️ partial | ✗ | ✗ | ✗ | | ReadableStream response | F | ✓ | ✗ | ✗ | ✗ | | SRI integrity | F | ✗ | ✗ | ✗ | ✗ | | GM options (proxy, progress, etc.) | F ² | ✗ | ✗ | ✗ | ✗ | | TypeScript types | F·L·M | ✓ | ✓ | ✗ | ✓ | | Last updated | 2026 | 2025 | 2025 | 2022 | 2020 |

² lite keeps the timeout + onprogress subset

Spec fields with no GM equivalent (mode, referrer, keepalive, priority, duplex, opaque-redirect, response.trailer) are unsupported across all of these libraries — see Limitations. Notable non-table differences: @trim21 corrupts binary bodies (reads via .text()), @sec-ant depends on vite-plugin-monkey, and @uwx ships a custom Response class at 4× our full size.


Security model

gmFetch runs through the userscript manager's privileged networking layer. This means:

  • CORS restrictions do not apply
  • Forbidden request headers can be sent freely
  • Cookies may be injected or observed across origins
  • Requests bypass page-level CSP and fetch restrictions

Users are responsible for respecting website policies, privacy, and applicable laws.


Changelog

1.4.0

  • Fix (lite + micro): a network failure mid-download used to resolve with a silently truncated (empty) body — the promise was fulfilled at HEADERS_RECEIVED and the error had nowhere to go. Resolution now waits for the complete body (onload) and rejects with TypeError("Failed to fetch") on mid-download errors. The reason for the early resolve — preserving the Location header of a redirect: "manual" 3xx, which some engines drop by DONE — is kept via a headers snapshot at HEADERS_RECEIVED (the same strategy the full variant always used).
  • Fix (micro): POST bodies were silently dropped in Firefox userscript engines (body presence gated on Request.body, which Firefox exposes as null). The 1.3.1 fix now applies to micro too.
  • Fix (all variants): null-body statuses (204/205/304) no longer throw inside the response callback (which left the promise permanently pending in lite/micro) when the engine hands over an empty-but-not-null blob.
  • Added (lite): gm.timeout — a timeout enforced by the GM layer, immune to background-tab timer throttling (an AbortSignal.timeout(5000) in an inactive tab can take a minute+; this one can't) — and gm.onprogress for download progress. ~90 B, both unavailable in any competing wrapper of this size class.
  • Added (micro): network errors now carry the raw GM event on error.cause (finalUrl, statusText, error detail), same as full/lite; response Set-Cookie is now readable via r.headers.getSetCookie() (the unfiltered Headers is re-stamped onto the Response, same trick full/lite use); response.type ("basic") and response.redirected are stamped too — full Response property parity with lite.
  • Internal: micro sends binary: true only when the payload actually is a Blob, and drops its redundant settled flag (a promise ignores repeat settle calls; micro has no cleanup tied to settling); the deliverBody/bodyReady machinery is gone from lite and micro (smaller bundles — the additions above fit well within what this freed).
  • Fix (full): aborting the signal after a streaming response resolved now cancels the underlying GM request (native-fetch parity: an abort cancels body reads too). Blob-mode behaviour unchanged.
  • Added (full): gm.context passthrough — the last remaining GM_xmlhttpRequest data option missing from the whitelist.
  • Build: terser gains ecma=2020, unsafe_arrows, unsafe_methods; new npm run build:size prints raw + gzip for all three IIFE bundles (also runs at the end of npm run build).

1.3.2

  • Internal: merged the string/URLSearchParams body branches in readBody (full + lite) — no behaviour change, slightly smaller output (full 3349 B, lite 2021 B IIFE).
  • Docs: refreshed size figures, collapsed the duplicated feature/comparison tables into one competitor-only comparison.

1.3.1

  • Fix (full + lite): POST bodies were silently dropped in Firefox userscript engines because body presence was detected via Request.body (which Firefox exposes as null even when a body is supplied). Detection now uses the original init.body. Endpoints that returned HTTP 400 on bodyless POSTs work again.

License

MIT.