Repository: unjs/h3
Pull request #848 (2024-07-23 19:14:00)

perf: `event.query` and `getQuery` with faster impl
Followup on #841

Parsing query params is costly and spec-compliant [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) (even with fast path optimization of #841) is not the fastest at least on Bun.

This PR tries another approach by providing `event.queryString` (fast path) and using an improved implementation of [anonrig/fast-querystring](https://github.com/anonrig/fast-querystring) via the existing `getQuery(event)` utility.

For simplicity, `event.searchParams` from #841 was renamed to `event.query`

Speedup is mainly benefiting Bun by ~%30 (latest node seems almost same perf) -- my guess is Bun is not optimized enough (for time being)

Bun:

<img width="615" alt="image" src="https://github.com/user-attachments/assets/57c397e0-c984-4544-9974-fe3bf24f237e">


Node.js:

<img width="590" alt="image" src="https://github.com/user-attachments/assets/5951b5c0-cfee-4230-9b01-f4df68112187">





Patch:
diff --git a/docs/2.utils/1.request.md b/docs/2.utils/1.request.md
index 0abc56c..d01caf1 100644
--- a/docs/2.utils/1.request.md
+++ b/docs/2.utils/1.request.md
@@ -27,7 +27,7 @@ app.use("/", (event) => {
 
 ### `getQuery(event)`
 
-Get query the params object from the request URL.
+Get parsed query string object from the request URL.
 
 **Example:**
 
diff --git a/eslint.config.mjs b/eslint.config.mjs
index e7c6fdc..2ced72e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -13,6 +13,7 @@ export default unjs(
       "@typescript-eslint/ban-types": "off",
       "unicorn/prefer-export-from": "off",
       "unicorn/prefer-string-raw": "off",
+      "unicorn/prefer-code-point": "off",
     },
   },
 );
diff --git a/package.json b/package.json
index 010842d..2bcb5f1 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "express": "^4.19.2",
     "get-port": "^7.1.0",
     "get-port-please": "^3.1.2",
-    "h3-nightly": "npm:h3-nightly@2.0.0-1721747307.095de7d",
+    "h3-nightly": "npm:h3-nightly@2.0.0-1721753961.6e9ba40",
     "h3-v1": "npm:h3@^1.12.0",
     "jiti": "2.0.0-beta.3",
     "listhen": "^1.7.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1cefb3a..4e5cfa7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -70,8 +70,8 @@ importers:
         specifier: ^3.1.2
         version: 3.1.2
       h3-nightly:
-        specifier: npm:h3-nightly@2.0.0-1721747307.095de7d
-        version: 2.0.0-1721747307.095de7d(crossws@0.2.4)
+        specifier: npm:h3-nightly@2.0.0-1721753961.6e9ba40
+        version: 2.0.0-1721753961.6e9ba40(crossws@0.2.4)
       h3-v1:
         specifier: npm:h3@^1.12.0
         version: h3@1.12.0
@@ -1693,8 +1693,8 @@ packages:
   graphemer@1.4.0:
     resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
 
-  h3-nightly@2.0.0-1721747307.095de7d:
-    resolution: {integrity: sha512-ynd/dcYd/B/OvGl616523U8Tih79xmSZjxC8fgVxGd+k1EGoak0tHPtvgZA1nYsv2xQXUcI5vWB6A2Era8xbyg==}
+  h3-nightly@2.0.0-1721753961.6e9ba40:
+    resolution: {integrity: sha512-YXIGQOpTDM1BbvllouqbZNfJ7gyC49Ryi/odF7KxjgGlOg7pJ7vjCeJeoEZD1GR1pwWISL1Ag+9F3IytVjKsyQ==}
     peerDependencies:
       crossws: ^0.2.4
     peerDependenciesMeta:
@@ -4797,7 +4797,7 @@ snapshots:
 
   graphemer@1.4.0: {}
 
-  h3-nightly@2.0.0-1721747307.095de7d(crossws@0.2.4):
+  h3-nightly@2.0.0-1721753961.6e9ba40(crossws@0.2.4):
     dependencies:
       cookie-es: 1.2.1
       rou3: 0.4.0
diff --git a/src/adapters/node/event.ts b/src/adapters/node/event.ts
index f227ee0..e976bd2 100644
--- a/src/adapters/node/event.ts
+++ b/src/adapters/node/event.ts
@@ -34,10 +34,14 @@ export const NodeEvent = /* @__PURE__ */ (() =>
       return this.url.pathname;
     }
 
-    get searchParams(): URLSearchParams {
+    get query(): URLSearchParams {
       return this.url.searchParams;
     }
 
+    get queryString(): string {
+      return this.url.search;
+    }
+
     get method(): HTTPMethod {
       return this.node.req.method as HTTPMethod;
     }
diff --git a/src/adapters/web/event.ts b/src/adapters/web/event.ts
index 11856ee..9effec2 100644
--- a/src/adapters/web/event.ts
+++ b/src/adapters/web/event.ts
@@ -7,8 +7,10 @@ export class WebEvent extends BaseEvent implements H3Event {
   response: H3EventResponse;
 
   _url?: URL;
-  _urlQindex?: number;
-  _searchParams?: URLSearchParams;
+  _pathname?: string;
+  _urlqindex?: number;
+  _query?: URLSearchParams;
+  _queryString?: string;
 
   constructor(request: Request, context?: H3EventContext) {
     super(context);
@@ -19,41 +21,60 @@ export class WebEvent extends BaseEvent implements H3Event {
   get url() {
     if (!this._url) {
       this._url = new URL(this.request.url);
-      this._searchParams = undefined;
     }
     return this._url;
   }
 
+  get path() {
+    return this.pathname + this.queryString;
+  }
+
   get pathname() {
     if (this._url) {
       return this._url.pathname; // reuse parsed URL
     }
-    const url = this.request.url;
-    const protoIndex = url.indexOf("://");
-    if (protoIndex === -1) {
-      return this.url.pathname; // deoptimize
+    if (!this._pathname) {
+      const url = this.request.url;
+      const protoIndex = url.indexOf("://");
+      if (protoIndex === -1) {
+        return this.url.pathname; // deoptimize
+      }
+      const pIndex = url.indexOf("/", protoIndex + 4 /* :// */);
+      if (pIndex === -1) {
+        return this.url.pathname; // deoptimize
+      }
+      const qIndex = (this._urlqindex = url.indexOf("?", pIndex));
+      this._pathname = url.slice(pIndex, qIndex === -1 ? undefined : qIndex);
     }
-    const pIndex = url.indexOf("/", protoIndex + 4 /* ://* */);
-    if (pIndex === -1) {
-      return this.url.pathname; // deoptimize
-    }
-    const qIndex = (this._urlQindex = url.indexOf("?", pIndex));
-    return url.slice(pIndex, qIndex === -1 ? undefined : qIndex);
+    return this._pathname;
   }
 
-  get searchParams() {
+  get query() {
     if (this._url) {
       return this._url.searchParams; // reuse parsed URL
     }
-    if (this._searchParams) {
-      return this._searchParams;
+    if (!this._query) {
+      this._query = new URLSearchParams(this.queryString);
+    }
+    return this._query;
+  }
+
+  get queryString() {
+    if (this._url) {
+      return this._url.search; // reuse parsed URL
     }
-    if (this._urlQindex === undefined) {
-      return this.url.searchParams; // deoptimize (mostly unlikely)
+    if (!this._queryString) {
+      const qIndex = this._urlqindex;
+      if (qIndex === -1) {
+        this._queryString = "";
+      } else {
+        this._queryString =
+          this._urlqindex === undefined
+            ? this.url.search // deoptimize (mostly unlikely as pathname accessor is always used)
+            : this.request.url.slice(this._urlqindex);
+      }
     }
-    const search = this.request.url.slice(this._urlQindex);
-    this._searchParams = new URLSearchParams(search);
-    return this._searchParams;
+    return this._queryString;
   }
 }
 
diff --git a/src/types/event.ts b/src/types/event.ts
index b8aa5dd..691256d 100644
--- a/src/types/event.ts
+++ b/src/types/event.ts
@@ -17,7 +17,8 @@ export interface H3Event<
   readonly method: HTTPMethod;
   readonly path: string;
   readonly pathname: string;
-  readonly searchParams: URLSearchParams;
+  readonly query: URLSearchParams;
+  readonly queryString: string;
   readonly url: URL;
   readonly headers: Headers;
   readonly request: Request;
diff --git a/src/utils/internal/encoding.ts b/src/utils/internal/encoding.ts
index 6a4ba1b..5a57efa 100644
--- a/src/utils/internal/encoding.ts
+++ b/src/utils/internal/encoding.ts
@@ -47,7 +47,7 @@ export function base64Encode(data: ArrayBuffer | Uint8Array | string): string {
       base64Code[(buff[i - 1]! & 0x0f) << 2],
     );
   }
-  // eslint-disable-next-line unicorn/prefer-code-point
+
   return String.fromCharCode(...bytes);
 }
 export function base64Decode(b64Url: string): Uint8Array {
@@ -60,7 +60,7 @@ export function base64Decode(b64Url: string): Uint8Array {
   const bytes = new Uint8Array(size);
   for (let i = 0; i < size; i++) {
     // (Uint8Array values are 0-255)
-    // eslint-disable-next-line unicorn/prefer-code-point
+
     bytes[i] = binString.charCodeAt(i);
   }
   return bytes;
diff --git a/src/utils/internal/iron-crypto.ts b/src/utils/internal/iron-crypto.ts
index 82d6bb2..2661720 100644
--- a/src/utils/internal/iron-crypto.ts
+++ b/src/utils/internal/iron-crypto.ts
@@ -313,7 +313,7 @@ function fixedTimeComparison(a: string, b: string): boolean {
   let mismatch = a.length === b.length ? 0 : 1;
   if (mismatch) b = a;
   for (let i = 0; i < a.length; i += 1)
-    mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i); // eslint-disable-line unicorn/prefer-code-point
+    mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
   return mismatch === 0;
 }
 
diff --git a/src/utils/internal/object.ts b/src/utils/internal/object.ts
index a4cfe44..44c281a 100644
--- a/src/utils/internal/object.ts
+++ b/src/utils/internal/object.ts
@@ -38,5 +38,9 @@ export function isJSONSerializable(value: any, _type: string): boolean {
 
   // Pure object
   const proto = Object.getPrototypeOf(value);
-  return proto === Object.prototype || proto === null;
+  return (
+    proto === Object.prototype ||
+    proto === null ||
+    Object.getPrototypeOf(proto) === null
+  );
 }
diff --git a/src/utils/internal/query.ts b/src/utils/internal/query.ts
new file mode 100644
index 0000000..e25022d
--- /dev/null
+++ b/src/utils/internal/query.ts
@@ -0,0 +1,135 @@
+/**
+Based on https://github.com/anonrig/fast-querystring/commit/9dcbaf
+Copyright (c) 2022 Yagiz Nizipli
+https://github.com/anonrig/fast-querystring/blob/main/LICENSE
+*/
+
+const plusRegex = /\+/g;
+
+const QueryParams = /* @__PURE__ */ (() => {
+  const C = function QueryParams() {};
+  C.prototype = Object.create(null);
+  return C;
+})() as unknown as { new (): Record<string, string | string[]> };
+
+export function parseQuery(input: string): Record<string, string | string[]> {
+  const params = new QueryParams();
+  if (!input || input === "?") {
+    return params;
+  }
+  const inputLength = input.length;
+
+  let key = "";
+  let value = "";
+  let startingIndex = -1;
+  let equalityIndex = -1;
+  let shouldDecodeKey = false;
+  let shouldDecodeValue = false;
+  let keyHasPlus = false;
+  let valueHasPlus = false;
+  let hasBothKeyValuePair = false;
+  let c = 0;
+
+  // Have a boundary of input.length + 1 to access last pair inside the loop.
+  for (let i = 0; i < inputLength + 1; i++) {
+    c = i === inputLength ? 38 /* & */ : input.charCodeAt(i);
+
+    // Handle '&' and end of line to pass the current values to result
+    switch (c) {
+      case 38 /* & */: {
+        hasBothKeyValuePair = equalityIndex > startingIndex;
+
+        // Optimization: Reuse equality index to store the end of key
+        if (!hasBothKeyValuePair) {
+          equalityIndex = i;
+        }
+
+        key = input.slice(startingIndex + 1, equalityIndex);
+
+        // Add key/value pair only if the range size is greater than 1; a.k.a. contains at least "="
+        if (hasBothKeyValuePair || key.length > 0) {
+          // Optimization: Replace '+' with space
+          if (keyHasPlus) {
+            key = key.replace(plusRegex, " ");
+          }
+
+          // Optimization: Do not decode if it's not necessary.
+          if (shouldDecodeKey) {
+            try {
+              key = decodeURIComponent(key);
+            } catch {
+              // Skip decoding
+            }
+          }
+
+          if (hasBothKeyValuePair) {
+            value = input.slice(equalityIndex + 1, i);
+
+            if (valueHasPlus) {
+              value = value.replace(plusRegex, " ");
+            }
+
+            if (shouldDecodeValue) {
+              try {
+                value = decodeURIComponent(value);
+              } catch {
+                // Skip decoding
+              }
+            }
+          }
+          const currentValue = params[key];
+
+          if (currentValue === undefined) {
+            params[key] = value;
+          } else {
+            if (Array.isArray(currentValue)) {
+              currentValue.push(value);
+            } else {
+              params[key] = [currentValue, value];
+            }
+          }
+        }
+
+        // Reset reading key value pairs
+        value = "";
+        startingIndex = i;
+        equalityIndex = i;
+        shouldDecodeKey = false;
+        shouldDecodeValue = false;
+        keyHasPlus = false;
+        valueHasPlus = false;
+
+        break;
+      }
+      case 61 /* = */: {
+        if (equalityIndex <= startingIndex) {
+          equalityIndex = i;
+        }
+        // If '=' character occurs again, we should decode the input.
+        else {
+          shouldDecodeValue = true;
+        }
+        break;
+      }
+      case 43 /* + */: {
+        if (equalityIndex > startingIndex) {
+          valueHasPlus = true;
+        } else {
+          keyHasPlus = true;
+        }
+        break;
+      }
+      case 37 /* % */: {
+        if (equalityIndex > startingIndex) {
+          shouldDecodeValue = true;
+        } else {
+          shouldDecodeKey = true;
+        }
+        break;
+      }
+      // No default
+    }
+  }
+
+  return params;
+}
diff --git a/src/utils/request.ts b/src/utils/request.ts
index a5c1a0a..a18a887 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -5,10 +5,11 @@ import type {
   ValidateFunction,
   H3Event,
 } from "../types";
+import { parseQuery } from "./internal/query";
 import { validateData } from "./internal/validate";
 
 /**
- * Get query the params object from the request URL.
+ * Get parsed query string object from the request URL.
  *
  * @example
  * app.use("/", (event) => {
@@ -20,7 +21,7 @@ export function getQuery<
   Event extends H3Event = H3Event,
   _T = Exclude<InferEventInput<"query", Event, T>, undefined>,
 >(event: Event): _T {
-  return Object.fromEntries(event.url.searchParams.entries()) as _T;
+  return parseQuery(event.queryString.slice(1)) as _T;
 }
 
 /**


Test patch:
diff --git a/test/bench/impl.ts b/test/bench/impl.ts
index af52bc4..965c6d8 100644
--- a/test/bench/impl.ts
+++ b/test/bench/impl.ts
@@ -8,7 +8,7 @@ export function createInstances() {
     // ["h3-nightly", h3(_h3nightly as any)],
     // ["h3-middleware", h3Middleware(_h3src)],
     // ["h3-v1", h3v1()],
-    ["std", std()],
+    // ["std", std()],
   ] as const;
 }
 
@@ -21,7 +21,9 @@ export function h3(lib: typeof _h3src) {
   // [GET] /id/:id
   app.get("/id/:id", (event) => {
     event.response.setHeader("x-powered-by", "benchmark");
-    return `${event.context.params!.id} ${event.searchParams.get("name")}`;
+    // const name = event.query.get("name");
+    const name = lib.getQuery(event).name;
+    return `${event.context.params!.id} ${name}`;
   });
 
   // [POST] /json
@@ -43,7 +45,8 @@ export function h3Middleware(lib: typeof _h3src) {
   // [GET] /id/:id
   app.use("/id/:id", (event) => {
     event.response.setHeader("x-powered-by", "benchmark");
-    return `${event.context.params!.id} ${event.url.searchParams.get("name")}`;
+    const name = lib.getQuery(event).name;
+    return `${event.context.params!.id} ${name}`;
   });
 
   // [POST] /json
diff --git a/test/sse.test.ts b/test/sse.test.ts
index aace421..c8658ae 100644
--- a/test/sse.test.ts
+++ b/test/sse.test.ts
@@ -6,7 +6,7 @@ import { describeMatrix } from "./_setup";
 describeMatrix("sse", (t, { it, expect }) => {
   beforeEach(() => {
     t.app.get("/sse", (event) => {
-      const includeMeta = event.searchParams.get("includeMeta") === "true";
+      const includeMeta = event.query.get("includeMeta") === "true";
       const eventStream = createEventStream(event);
       let counter = 0;
       const clear = setInterval(() => {
diff --git a/test/unit/query.test.ts b/test/unit/query.test.ts
new file mode 100644
index 0000000..f52f157
--- /dev/null
+++ b/test/unit/query.test.ts
@@ -0,0 +1,215 @@
+/**
+Based on https://github.com/anonrig/fast-querystring/commit/9dcbaf
+Copyright (c) 2022 Yagiz Nizipli
+https://github.com/anonrig/fast-querystring/blob/main/LICENSE
+*/
+
+import { assert, describe, test } from "vitest";
+import vm from "node:vm";
+import { parseQuery } from "../../src/utils/internal/query";
+
+function extendedFunction() {}
+extendedFunction.prototype = { a: "b" };
+
+function createWithNoPrototype(properties: any) {
+  const noProto = Object.create(null);
+  for (const property of properties) {
+    noProto[property.key] = property.value;
+  }
+  return noProto;
+}
+export const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})');
+
+const qsNoMungeTestCases = [
+  ["", {}],
+  ["foo=bar&foo=baz", { foo: ["bar", "baz"] }],
+  ["foo=bar&foo=baz", foreignObject],
+  ["blah=burp", { blah: "burp" }],
+  ["a=!-._~'()*", { a: "!-._~'()*" }],
+  ["a=abcdefghijklmnopqrstuvwxyz", { a: "abcdefghijklmnopqrstuvwxyz" }],
+  ["a=ABCDEFGHIJKLMNOPQRSTUVWXYZ", { a: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }],
+  ["a=0123456789", { a: "0123456789" }],
+  ["gragh=1&gragh=3&goo=2", { gragh: ["1", "3"], goo: "2" }],
+  [
+    "frappucino=muffin&goat%5B%5D=scone&pond=moose",
+    { frappucino: "muffin", "goat[]": "scone", pond: "moose" },
+  ],
+  ["trololol=yes&lololo=no", { trololol: "yes", lololo: "no" }],
+];
+
+const qsTestCases = [
+  [
+    "__proto__=1",
+    "__proto__=1",
+    createWithNoPrototype([{ key: "__proto__", value: "1" }]),
+  ],
+  [
+    "__defineGetter__=asdf",
+    "__defineGetter__=asdf",
+    JSON.parse('{"__defineGetter__":"asdf"}'),
+  ],
+  [
+    "foo=918854443121279438895193",
+    "foo=918854443121279438895193",
+    { foo: "918854443121279438895193" },
+  ],
+  ["foo=bar", "foo=bar", { foo: "bar" }],
+  ["foo=bar&foo=quux", "foo=bar&foo=quux", { foo: ["bar", "quux"] }],
+  ["foo=1&bar=2", "foo=1&bar=2", { foo: "1", bar: "2" }],
+  [
+    "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F",
+    "my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F",
+    { "my weird field": "q1!2\"'w$5&7/z8)?" },
+  ],
+  ["foo%3Dbaz=bar", "foo%3Dbaz=bar", { "foo=baz": "bar" }],
+  ["foo=baz=bar", "foo=baz%3Dbar", { foo: "baz=bar" }],
+  [
+    "str=foo&arr=1&arr=2&arr=3&somenull=&undef=",
+    "str=foo&arr=1&arr=2&arr=3&somenull=&undef=",
+    {
+      str: "foo",
+      arr: ["1", "2", "3"],
+      somenull: "",
+      undef: "",
+    },
+  ],
+  [" foo = bar ", "%20foo%20=%20bar%20", { " foo ": " bar " }],
+  ["foo=%zx", "foo=%25zx", { foo: "%zx" }],
+  ["foo=%EF%BF%BD", "foo=%EF%BF%BD", { foo: "\uFFFD" }],
+  // See: https://github.com/joyent/node/issues/1707
+  [
+    "hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz",
+    "hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz",
+    {
+      hasOwnProperty: "x",
+      toString: "foo",
+      valueOf: "bar",
+      __defineGetter__: "baz",
+    },
+  ],
+  // See: https://github.com/joyent/node/issues/3058
+  ["foo&bar=baz", "foo=&bar=baz", { foo: "", bar: "baz" }],
+  ["a=b&c&d=e", "a=b&c=&d=e", { a: "b", c: "", d: "e" }],
+  ["a=b&c=&d=e", "a=b&c=&d=e", { a: "b", c: "", d: "e" }],
+  ["a=b&=c&d=e", "a=b&=c&d=e", { a: "b", "": "c", d: "e" }],
+  ["a=b&=&c=d", "a=b&=&c=d", { a: "b", "": "", c: "d" }],
+  ["&&foo=bar&&", "foo=bar", { foo: "bar" }],
+  ["&", "", {}],
+  ["&&&&", "", {}],
+  ["&=&", "=", { "": "" }],
+  ["&=&=", "=&=", { "": ["", ""] }],
+  ["=", "=", { "": "" }],
+  ["+", "%20=", { " ": "" }],
+  ["+=", "%20=", { " ": "" }],
+  ["+&", "%20=", { " ": "" }],
+  ["=+", "=%20", { "": " " }],
+  ["+=&", "%20=", { " ": "" }],
+  ["a&&b", "a=&b=", { a: "", b: "" }],
+  ["a=a&&b=b", "a=a&b=b", { a: "a", b: "b" }],
+  ["&a", "a=", { a: "" }],
+  ["&=", "=", { "": "" }],
+  ["a&a&", "a=&a=", { a: ["", ""] }],
+  ["a&a&a&", "a=&a=&a=", { a: ["", "", ""] }],
+  ["a&a&a&a&", "a=&a=&a=&a=", { a: ["", "", "", ""] }],
+  ["a=&a=value&a=", "a=&a=value&a=", { a: ["", "value", ""] }],
+  ["foo+bar=baz+quux", "foo%20bar=baz%20quux", { "foo bar": "baz quux" }],
+  ["+foo=+bar", "%20foo=%20bar", { " foo": " bar" }],
+  ["a+", "a%20=", { "a ": "" }],
+  ["=a+", "=a%20", { "": "a " }],
+  ["a+&", "a%20=", { "a ": "" }],
+  ["=a+&", "=a%20", { "": "a " }],
+  ["%20+", "%20%20=", { "  ": "" }],
+  ["=%20+", "=%20%20", { "": "  " }],
+  ["%20+&", "%20%20=", { "  ": "" }],
+  ["=%20+&", "=%20%20", { "": "  " }],
+  [null, "", {}],
+  [undefined, "", {}],
+];
+
+const qsWeirdObjects = [
+  [{ regexp: /./g }, "regexp=", { regexp: "" }],
+  [{ regexp: /./g }, "regexp=", { regexp: "" }],
+  [{ fn: () => {} }, "fn=", { fn: "" }],
+  [{ fn: new Function("") }, "fn=", { fn: "" }],
+  [{ math: Math }, "math=", { math: "" }],
+  [{ e: extendedFunction }, "e=", { e: "" }],
+  [{ d: new Date() }, "d=", { d: "" }],
+  [{ d: Date }, "d=", { d: "" }],
+  [{ f: Boolean(false), t: Boolean(true) }, "f=&t=", { f: "", t: "" }],
+  [{ f: false, t: true }, "f=false&t=true", { f: "false", t: "true" }],
+  [{ n: null }, "n=", { n: "" }],
+  [{ nan: Number.NaN }, "nan=", { nan: "" }],
+  [{ inf: Number.POSITIVE_INFINITY }, "inf=", { inf: "" }],
+  [{ a: [], b: [] }, "", {}],
+  [{ a: 1, b: [] }, "a=1", { a: "1" }],
+];
+
+describe("node.js tests", () => {
+  describe("qs", () => {
+    for (const t of qsTestCases) {
+      test(t[0], () => {
+        assert.deepEqual(parseQuery(t[0]), t[2]);
+      });
+    }
+  });
+
+  describe("no mangle", () => {
+    for (const t of qsNoMungeTestCases) {
+      test(t[0], () => {
+        assert.deepEqual(parseQuery(t[0]), t[1]);
+      });
+    }
+  });
+
+  describe("weird", () => {
+    for (const t of qsWeirdObjects)
+      test(JSON.stringify(t[1]), () => {
+        assert.deepEqual(
+          parseQuery(t[1] as string),
+          t[2] as Record<string, unknown>,
+        );
+      });
+  });
+});
+
+test("handles & on first/last character", () => {
+  assert.deepEqual(parseQuery("&hello=world"), { hello: "world" });
+  assert.deepEqual(parseQuery("hello=world&"), { hello: "world" });
+});
+
+test("handles ? on first character", () => {
+  // This aligns with `node:querystring` functionality
+  assert.deepEqual(parseQuery("?hello=world"), { "?hello": "world" });
+});
+
+test("handles + character", () => {
+  assert.deepEqual(parseQuery("author=Yagiz+Nizipli"), {
+    author: "Yagiz Nizipli",
+  });
+});
+
+test("should accept pairs with missing values", () => {
+  assert.deepEqual(parseQuery("foo=bar&hey"), { foo: "bar", hey: "" });
+  assert.deepEqual(parseQuery("hey"), { hey: "" });
+});
+
+test("should decode key", () => {
+  assert.deepEqual(parseQuery("invalid%key=hello"), { "invalid%key": "hello" });
+  assert.deepEqual(parseQuery("full%20name=Yagiz"), { "full name": "Yagiz" });
+});
+
+test("should handle really large object", () => {
+  const q = new URLSearchParams();
+  for (let i = 0; i < 2000; i++) {
+    q.set(i.toString(), i.toString());
+  }
+  const url = q.toString();
+  assert.strictEqual(Object.keys(parseQuery(url)).length, 2000);
+});
+
+test("should parse large numbers", () => {
+  assert.strictEqual(
+    parseQuery("id=918854443121279438895193").id,
+    "918854443121279438895193",
+  );
+});