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.3.1

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.0 KB min, ~1.0 KB gzip) — core fetch semantics, AbortSignal, forbidden headers
  • Micro (~0.8 KB min, ~0.5 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 | ✓ | ✓ | ✗ | | Error semantics (DOMException types) | ✓ | ✓ | ✗ | | Cache mode mapping | ✓ | ✗ | ✗ | | ReadableStream response | ✓ | ✗ | ✗ | | SRI integrity verification | ✓ | ✗ | ✗ | | GM options (cookie, proxy, timeout, etc.) | ✓ | ✗ | ✗ | | Upload/download progress | ✓ | ✗ | ✗ | | Early headers via onreadystatechange | ✓ | ✗ | ✗ |

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

Bundler (ES module)

import gmFetch from "@kobzi/gmfetch";       // full
import gmFetch from "@kobzi/gmfetch/lite";   // lite
import gmFetch from "@kobzi/gmfetch/micro";  // micro

CDN (IIFE, no bundler)

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

const r = await gmFetch("https://example.com/api");

Or for lite/micro:

// @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

The IIFE exposes gmFetch as a global. Pin to a specific version for stability.

@grant GM_xmlhttpRequest (or GM.xmlHttpRequest) required. @connect <domain> required for cross-origin.

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(). The full version extends init with an optional gm field:

interface GmFetchInit extends RequestInit {
  gm?: GmOptions;  // full 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 (full only)

Only whitelisted keys are forwarded (protects internal callbacks):

| 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"). |


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

Sending cookies via headers works in full and lite (forbidden headers preserved). In micro, cookies via headers only work if the browser doesn't strip them (use full/lite for reliable cookie injection). The gm.cookie option (additive patching) is full only.

| 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
await gmFetch(url, { signal: AbortSignal.timeout(5000) });

// gm.timeout — immune to tab throttling (full only)
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 full and lite, 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 throws a bare TypeError("Failed to fetch") with no cause.


Examples

POST JSON (all variants)

const r = await gmFetch("https://api.example.com/items", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "thing" }),
});

Forbidden headers (full and lite)

const r = await gmFetch("https://example.com/protected", {
  headers: {
    "Cookie": "session=abc; user_id=42",
    "User-Agent": "Mozilla/5.0 (custom)",
    "Referer": "https://example.com/login",
  },
});

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",
});

Full example (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" }),
    signal: AbortSignal.timeout(15_000),
    cache: "no-store",
    gm: { onprogress: ({ loaded }) => console.log(loaded) },
  });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  console.log(await r.json());
})();

Lite example

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

(async () => {
  const r = await gmFetch("https://api.example.com/data", {
    method: "POST",
    headers: { "Cookie": "auth=abc", "Content-Type": "application/json" },
    body: JSON.stringify({ query: "test" }),
    signal: AbortSignal.timeout(10_000),
  });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  console.log(await r.json());
})();

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.esm.min.js          3364 B  (full, ESM)
├── gmFetch.iife.min.js         3383 B  (full, IIFE)
├── gmFetch.lite.esm.min.js     2036 B  (lite, ESM)
├── gmFetch.lite.iife.min.js    2055 B  (lite, IIFE)
├── gmFetch.micro.esm.min.js     808 B  (micro, ESM)
├── gmFetch.micro.iife.min.js    813 B  (micro, IIFE)
├── gmFetch.d.ts                (types, full)
├── gmFetch.lite.d.ts           (types, lite)
└── gmFetch.micro.d.ts          (types, micro)

Sizes (esbuild + terser, minified):

| Variant | Raw (ESM) | Raw (IIFE) | Gzip (IIFE) | Brotli (IIFE) | |---|---:|---:|---:|---:| | Full | 3364 B | 3383 B | 1723 B | 1530 B | | Lite | 2036 B | 2055 B | 1051 B | 925 B | | Micro | 808 B | 813 B | 524 B | 442 B |

For comparison (IIFE, minified): | Library | Size | Notes | |---|---|---| | @kobzi/gmfetch micro | ~0.8 KB | absolute minimum, no abort | | gmxhr-fetch | 0.9 KB | ultra-minimal, no AbortSignal, no types, unmaintained | | @sec-ant/gm-fetch | 1.9 KB | includes vite-plugin-monkey runtime | | @kobzi/gmfetch lite | 2.0 KB | own terser build, more correct | | @trim21/gm-fetch | 2.1 KB | minified by jsdelivr (no own min build) | | @kobzi/gmfetch full | 3.3 KB | own terser build, full GM API surface | | @uwx/gm-fetch | 12.4 KB | not minified, custom Response class |

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

| Feature | Fetch spec | GM specific | @kobzi full | @kobzi lite | @kobzi micro | @sec-ant | @trim21 | gmxhr-fetch | @uwx/gm-fetch | |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | Size (IIFE, min) | — | — | 3.3 KB | 2.0 KB | ~0.8 KB | 1.9 KB | 2.1 KB | 0.9 KB | 12.4 KB | | Dependencies | — | — | 0 | 0 | 0 | vite-plugin-monkey | 0 | 0 | 0 | | Request normalisation | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | | AbortSignal + cleanup | ✓ | | ✓ | ✓ | ✗ | ⚠️ leak | ⚠️ leak | ✗ | ✗ | | signal.reason propagation | ✓ | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | | Double-settle guard | ✓ | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | | status:0 → TypeError | ✓ | | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | | Timeout → TimeoutError | ✓ | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | | Forbidden headers | | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | | Response url/type/redirected | ✓ | | ✓ | ✓ | url only | ✓ | ⚠️ inverted | ✗ | ✓ | | Set-Cookie preservation | | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | | credentials → anonymous | ✓ | | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | | redirect passthrough | ✓ | | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | | Cache mode mapping | ✓ | | ✓ | ✗ | ✗ | ⚠️ partial | ✗ | ✗ | ✗ | | ReadableStream response | ✓ | | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | | SRI integrity | ✓ | | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | GM options (cookie, proxy, etc.) | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | Upload progress | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | Download progress | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | Background fetch (MV3) | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | maxRedirects | | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | Binary body support | ✓ | | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | | Empty body → undefined (no empty Blob sent) | ✓ | | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | | RFC 7230 header folding | ✓ | | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | | onerror → generic TypeError (spec) | ✓ | | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | | Cross-realm stream detection | | ✓ | ✓ | — | — | ✗ | — | — | — | | TypeScript types included | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | | Last updated | | | 2026 | 2026 | 2026 | 2025 | 2025 | 2022 | 2020 | | duplex (upload streaming) | ✓ | | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | | mode (cors/no-cors/same-origin) | ✓ | | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | | referrer / referrerPolicy | ✓ | | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | | keepalive | ✓ | | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | | priority | ✓ | | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | ✗¹ | | opaqueredirect response | ✓ | | ✗² | ✗² | ✗² | ✗² | ✗² | ✗² | | response.trailer | ✓ | | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |

¹ Silently ignored — no GM_xmlhttpRequest equivalent exists. ² GM returns full 3xx response with headers/body instead of opaque redirect.

Key advantages over alternatives:

  • vs @sec-ant: forbidden headers support, AbortSignal cleanup (no memory leak), signal.reason propagation, no vite-plugin-monkey dependency
  • vs @trim21: correct binary body handling (trim21 corrupts via .text()), proper header parsing, correct redirected flag, Set-Cookie access, smaller when minified
  • vs gmxhr-fetch: AbortSignal, Request normalisation, Response properties, TypeScript, error semantics — gmxhr-fetch is a bare-minimum wrapper with no spec compliance
  • vs @uwx/gm-fetch: 4x smaller (3.3 vs 12.4 KB), minified build included, no custom Response class overhead

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.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.