{"_rev":"4-81a5599484ef42ca2d997d7146469f15","time":{"created":"2026-02-24T11:30:13.870Z","modified":"2026-02-24T11:30:14.686Z","1.0.0":"2026-02-24T10:55:15.087Z","1.0.0-rc.1":"2026-02-24T11:30:14.479Z"},"_id":"@sptzx/request","name":"@sptzx/request","dist-tags":{"latest":"1.0.0-rc.1"},"versions":{"1.0.0-rc.1":{"name":"@sptzx/request","version":"1.0.0-rc.1","description":"Tiny, Elegant, and Brutally Optimized HTTP Client for Server-Side Runtimes. Zero-dependency.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"scripts":{"build":"tsc","build:watch":"tsc --watch","test":"node test/_harness.js"},"devDependencies":{"typescript":"^5.4.0"},"engines":{"node":">=18.0.0"},"keywords":["fetch","http","client","node","bun","deno","zero-dependency","cookie-jar","circuit-breaker","retry","ndjson"],"license":"MIT","author":{"name":"siputzx"},"repository":{"type":"git","url":"git+https://github.com/siputzx/request.git"},"homepage":"https://github.com/siputzx/request#readme","bugs":{"url":"https://github.com/siputzx/request/issues"},"publishConfig":{"access":"public"},"_id":"@sptzx/request@1.0.0-rc.1","gitHead":"2ee2548f75731c5262857342abbccafafdc30f4d","_nodeVersion":"20.20.0","_npmVersion":"10.8.2","dist":{"integrity":"sha512-P3OrJj1gSrULdKafIufEPXpHAR0S1hOW395u0Ywa8Cgy8N8P20MOJhx8Le9pqSiqJFJI+h7HA6+A/B6byYoStw==","shasum":"22168d29845a6355f6c4247f255b429bfd4fef0d","tarball":"https://registry.npmjs.org/@sptzx/request/-/request-1.0.0-rc.1.tgz","fileCount":115,"unpackedSize":195012,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDwINO2MAtQDY6LP+Vs/STufFck23wZxi21ZTr64wUKvQIhAN7yq7plRrR5vgJtJOOyXEzo2aQ+MILasphBjM5LCqwz"}]},"_npmUser":{"name":"putuofc","email":"siputzx.id@gmail.com"},"directories":{},"maintainers":[{"name":"putuofc","email":"siputzx.id@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/request_1.0.0-rc.1_1771932614322_0.570479633231255"},"_hasShrinkwrap":false}},"maintainers":[{"name":"putuofc","email":"siputzx.id@gmail.com"}],"description":"Tiny, Elegant, and Brutally Optimized HTTP Client for Server-Side Runtimes. Zero-dependency.","homepage":"https://github.com/siputzx/request#readme","keywords":["fetch","http","client","node","bun","deno","zero-dependency","cookie-jar","circuit-breaker","retry","ndjson"],"repository":{"type":"git","url":"git+https://github.com/siputzx/request.git"},"author":{"name":"siputzx"},"bugs":{"url":"https://github.com/siputzx/request/issues"},"license":"MIT","readme":"<div align=\"center\">\n\n# @sptzx/request\n\n**A zero-dependency HTTP client for server-side runtimes — built on top of the native Fetch API.**\n\n[![npm version](https://img.shields.io/npm/v/%40sptzx%2Frequest.svg?style=flat-square)](https://www.npmjs.com/package/@sptzx/request)\n[![license](https://img.shields.io/badge/license-MIT%20%2F%20BSD--3--Clause-blue.svg?style=flat-square)](#license)\n[![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square)](package.json)\n[![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-success.svg?style=flat-square)](https://nodejs.org)\n[![Bun](https://img.shields.io/badge/bun-supported-f472b6.svg?style=flat-square)](https://bun.sh)\n[![Deno](https://img.shields.io/badge/deno-supported-000000.svg?style=flat-square)](https://deno.land)\n\n</div>\n\n---\n\n`@sptzx/request` is a production-grade HTTP client that wraps the native `fetch` API with everything server-side code actually needs: smart retries, automatic cookie persistence, circuit breaking, NDJSON streaming, proxy support, and lifecycle hooks — all with **zero external dependencies**.\n\nIt is built for **Node.js ≥ 18**, **Bun**, and **Deno**, and ships as fully typed TypeScript source alongside a compiled `dist/`.\n\n---\n\n## Table of Contents\n\n- [Features](#-features)\n- [Installation](#-installation)\n- [Quick Start](#-quick-start)\n- [Usage](#-usage)\n  - [Basic Requests](#basic-requests)\n  - [Cookie Jar (Session Management)](#cookie-jar-session-management)\n  - [Retry & Circuit Breaker](#retry--circuit-breaker)\n  - [Smart Redirects](#smart-redirects)\n  - [NDJSON Streaming](#ndjson-streaming)\n  - [Instance Configuration](#instance-configuration)\n  - [Upload & Download Progress](#upload--download-progress)\n  - [Error Handling](#error-handling)\n- [API Reference](#-api-reference)\n- [Credits](#-credits)\n- [License](#-license)\n\n---\n\n## ✨ Features\n\n| Feature | Description |\n|---|---|\n| **Zero Dependencies** | No `node_modules` at runtime. Ever. |\n| **Smart Retry** | Exponential backoff, per-status retry rules, `Retry-After` header support, and jitter |\n| **Cookie Jar** | RFC 6265-compliant automatic cookie management across requests and redirects |\n| **Circuit Breaker** | CLOSED → OPEN → HALF_OPEN state machine to protect upstream services |\n| **Smart Redirects** | Native redirect for speed; manual redirect when a CookieJar is active for correctness |\n| **NDJSON Streaming** | First-class `AsyncGenerator` for newline-delimited JSON (LLMs, event streams) |\n| **Lifecycle Hooks** | `beforeRequest`, `beforeRetry`, `afterResponse`, `beforeError` |\n| **Progress Callbacks** | Streaming upload and download progress |\n| **Proxy Support** | `proxyUrl` shorthand or raw Undici `dispatcher` |\n| **OOM Protection** | Error body reads capped at 10 MB, racing against active timeout |\n| **Full TypeScript** | 100% typed — every option, hook, error, and return value |\n\n---\n\n## 📦 Installation\n\n```bash\n# npm\nnpm install @sptzx/request\n\n# yarn\nyarn add @sptzx/request\n\n# pnpm\npnpm add @sptzx/request\n\n# bun\nbun add @sptzx/request\n```\n\n> **Runtime requirements:** Node.js ≥ 18.0.0, Bun (any version), or Deno (any version).\n\n---\n\n## ⚡ Quick Start\n\n```typescript\nimport request from '@sptzx/request';\n\n// GET and parse JSON\nconst users = await request.get('https://api.example.com/users').json();\n\n// POST with JSON body\nconst created = await request.post('https://api.example.com/users', {\n  json: { name: 'Alice', role: 'admin' },\n}).json();\n\n// Automatic retry on failure (default: 2 retries)\nconst data = await request.get('https://api.example.com/data', {\n  retry: 3,\n  timeout: 5_000,\n}).json();\n```\n\n---\n\n## 🚀 Usage\n\n### Basic Requests\n\n```typescript\nimport request from '@sptzx/request';\n\n// GET\nconst data = await request.get('https://api.example.com/users').json<User[]>();\n\n// GET with query parameters\nconst results = await request.get('https://api.example.com/search', {\n  searchParams: { q: 'nodejs', page: '1', limit: '20' },\n}).json();\n\n// POST — JSON body\nconst user = await request.post('https://api.example.com/users', {\n  json: { name: 'Alice', role: 'admin' },\n}).json<User>();\n\n// PUT / PATCH / DELETE\nawait request.put('https://api.example.com/users/1', { json: { name: 'Bob' } });\nawait request.patch('https://api.example.com/users/1', { json: { role: 'viewer' } });\nawait request.delete('https://api.example.com/users/1');\n\n// Access the raw Response\nconst response = await request.get('https://api.example.com/health');\nconsole.log(response.status);                          // 200\nconsole.log(response.headers.get('x-request-id'));\n\n// Other body types\nconst text   = await request.get('https://api.example.com/report').text();\nconst buffer = await request.get('https://files.example.com/file.bin').arrayBuffer();\nconst blob   = await request.get('https://files.example.com/image.png').blob();\n```\n\n---\n\n### Cookie Jar (Session Management)\n\n`@sptzx/request` includes a built-in RFC 6265-compliant `CookieJar`. When attached to an instance, it automatically stores `Set-Cookie` response headers and injects the correct `Cookie` header on all subsequent requests — including across redirects and retries.\n\n```typescript\nimport request, { CookieJar } from '@sptzx/request';\n\nconst jar = new CookieJar();\nconst session = request.extend({ cookieJar: jar });\n\n// Login — the response's Set-Cookie is stored automatically\nawait session.post('https://api.example.com/auth/login', {\n  json: { username: 'alice', password: 's3cr3t' },\n});\n\n// All subsequent requests send the stored cookie automatically\nconst profile = await session.get('https://api.example.com/me').json();\nconst orders  = await session.get('https://api.example.com/orders').json();\n\n// Logout — jar is cleared when the session cookie expires\nawait session.post('https://api.example.com/auth/logout');\n```\n\n---\n\n### Retry & Circuit Breaker\n\n```typescript\nimport request, { CircuitBreaker } from '@sptzx/request';\n\nconst breaker = new CircuitBreaker({\n  threshold:        5,      // Open after 5 consecutive failures\n  halfOpenAfterMs:  15_000, // Try again after 15 seconds\n  successThreshold: 2,      // Close again after 2 successful probes\n  onStateChange: (from, to) => console.log(`Circuit: ${from} → ${to}`),\n});\n\nconst api = request.extend({\n  prefixUrl:      'https://api.example.com',\n  timeout:        8_000,\n  circuitBreaker: breaker,\n  retry: {\n    limit:            3,\n    methods:          ['get', 'post', 'put'],\n    statusCodes:      [408, 429, 502, 503, 504],\n    afterStatusCodes: [429],          // Respect Retry-After header on 429\n    backoffLimit:     30_000,         // Cap individual delay at 30 s\n    jitter:           true,           // Randomise delay to avoid thundering-herd\n    delay: (attempt) => 300 * 2 ** (attempt - 1), // 300ms, 600ms, 1200ms...\n  },\n  hooks: {\n    beforeRequest: [\n      ({ request }) => {\n        request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`);\n      },\n    ],\n    beforeRetry: [\n      ({ error, retryCount }) => {\n        console.warn(`[retry #${retryCount}] ${error.message}`);\n      },\n    ],\n  },\n});\n\nconst data = await api.get('users').json();\n```\n\n---\n\n### Smart Redirects\n\n`@sptzx/request` automatically selects the right redirect strategy based on your configuration:\n\n- **Native redirect** (default) — when no `cookieJar` is active, redirects are delegated entirely to the runtime's native `fetch`. Zero overhead.\n- **Manual redirect** (with `cookieJar`) — when a `cookieJar` is active, every redirect hop is intercepted in userland. This is required because native `fetch` silently drops `Set-Cookie` headers on intermediate hops, which would corrupt session state.\n\nDuring manual redirect, `@sptzx/request`:\n1. Persists `Set-Cookie` from each intermediate response to the jar.\n2. Injects the correct `Cookie` header for the next hop URL.\n3. Rewrites `POST` → `GET` on `303 See Other` (per RFC 7231).\n4. Preserves the method on `307 Temporary Redirect` and `308 Permanent Redirect`.\n5. Throws if the number of hops exceeds `maxRedirects` (default: `10`).\n\n```typescript\nimport request, { CookieJar } from '@sptzx/request';\n\nconst jar     = new CookieJar();\nconst session = request.extend({ cookieJar: jar });\n\n// Cookies from 302 intermediate hops are captured automatically\nawait session.get('https://api.example.com/login');\n\n// Limit redirect depth\nawait session.get('https://api.example.com/page', { maxRedirects: 5 });\n```\n\n---\n\n### NDJSON Streaming\n\nIdeal for consuming LLM token streams, server-sent database diffs, or any newline-delimited JSON endpoint.\n\n```typescript\nimport request from '@sptzx/request';\n\nconst stream = request.post('https://api.example.com/llm/completions', {\n  json: { model: 'gpt-4', prompt: 'Hello', stream: true },\n  timeout: false, // Disable timeout for long-running streams\n});\n\nfor await (const chunk of stream.ndjson<{ token: string; done: boolean }>()) {\n  process.stdout.write(chunk.token);\n  if (chunk.done) break;\n}\n```\n\n---\n\n### Instance Configuration\n\nUse `extend()` to create pre-configured instances with shared defaults. Instances inherit and deep-merge from their parent — ideal for building service-specific clients once and reusing them everywhere.\n\n```typescript\nimport request from '@sptzx/request';\n\n// Base HTTP client\nconst http = request.extend({\n  timeout: 10_000,\n  retry:   { limit: 2 },\n  headers: { 'User-Agent': 'MyApp/2.0' },\n});\n\n// GitHub API client (extends http)\nconst github = http.extend((parent) => ({\n  ...parent,\n  prefixUrl: 'https://api.github.com/',\n  headers: {\n    ...parent.headers,\n    Authorization: `token ${process.env.GITHUB_TOKEN}`,\n    Accept: 'application/vnd.github+json',\n  },\n}));\n\n// Stripe API client (extends http)\nconst stripe = http.extend({\n  prefixUrl: 'https://api.stripe.com/v1/',\n  headers: {\n    Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n  },\n});\n\nconst repos   = await github.get('user/repos').json();\nconst charges = await stripe.get('charges').json();\n```\n\n---\n\n### Upload & Download Progress\n\n```typescript\nimport request from '@sptzx/request';\n\n// Download with progress\nconst response = await request.get('https://files.example.com/dataset.zip', {\n  onDownloadProgress: ({ percent, transferredBytes, totalBytes }) => {\n    process.stdout.write(`\\rDownloading: ${Math.round(percent * 100)}%`);\n  },\n});\nconst buffer = await response.arrayBuffer();\n\n// Upload with progress\nawait request.post('https://api.example.com/upload', {\n  body: fileStream,\n  onUploadProgress: ({ percent, transferredBytes }) => {\n    process.stdout.write(`\\rUploading: ${Math.round(percent * 100)}%`);\n  },\n});\n```\n\n---\n\n### Error Handling\n\n```typescript\nimport request, {\n  HTTPError,\n  TimeoutError,\n  CircuitBreakerOpenError,\n  isRequestError,\n} from '@sptzx/request';\n\ntry {\n  const data = await request.get('https://api.example.com/resource').json();\n} catch (error) {\n  if (error instanceof HTTPError) {\n    // error.response — the raw Response object\n    // error.data     — parsed response body (JSON or text)\n    console.error(`HTTP ${error.response.status}:`, error.data);\n\n  } else if (error instanceof TimeoutError) {\n    console.error('Request timed out after', error.request.url);\n\n  } else if (error instanceof CircuitBreakerOpenError) {\n    console.error('Circuit breaker is OPEN — request skipped');\n\n  } else if (isRequestError(error)) {\n    // Catches any @sptzx/request error (umbrella guard)\n    console.error('Request error:', error.message);\n\n  } else {\n    throw error; // Re-throw unrecognised errors\n  }\n}\n```\n\nTo disable automatic error throwing and handle responses manually:\n\n```typescript\nconst response = await request.get('https://api.example.com/resource', {\n  throwHttpErrors: false,\n});\n\nif (!response.ok) {\n  const body = await response.json();\n  console.error(`Error ${response.status}:`, body);\n}\n```\n\n---\n\n## 🗂️ API Reference\n\n### HTTP Methods\n\n```typescript\nrequest(url, options?)          // Generic request\nrequest.get(url, options?)\nrequest.post(url, options?)\nrequest.put(url, options?)\nrequest.patch(url, options?)\nrequest.delete(url, options?)\nrequest.head(url, options?)\n```\n\nAll methods return a `ResponsePromise` — a native `Promise<Response>` extended with body helpers:\n\n| Method | Returns |\n|---|---|\n| `.json<T>()` | `Promise<T>` |\n| `.text()` | `Promise<string>` |\n| `.blob()` | `Promise<Blob>` |\n| `.arrayBuffer()` | `Promise<ArrayBuffer>` |\n| `.formData()` | `Promise<FormData>` |\n| `.bytes()` | `Promise<Uint8Array>` |\n| `.ndjson<T>()` | `AsyncGenerator<T>` |\n\n### Instance Methods\n\n| Method | Description |\n|---|---|\n| `request.extend(defaults \\| fn)` | Create a new instance that inherits and merges from the current instance |\n| `request.create(defaults)` | Create a new instance with only the given defaults (no inheritance) |\n\n### Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `prefixUrl` | `string \\| URL` | `''` | Base URL prepended to every request path |\n| `method` | `string` | `'GET'` | HTTP method |\n| `headers` | `HeadersInit` | `{}` | Request headers |\n| `json` | `unknown` | — | JSON body — auto-sets `Content-Type: application/json` |\n| `searchParams` | `string \\| Record \\| URLSearchParams` | — | Query string parameters |\n| `timeout` | `number \\| false` | `10000` | Wall-clock timeout (ms) for the entire operation including retries |\n| `retry` | `RetryOptions \\| number` | `2` | Retry limit or full retry configuration |\n| `cookieJar` | `CookieJar` | — | Enables automatic RFC 6265 cookie management |\n| `circuitBreaker` | `CircuitBreaker` | — | Circuit breaker instance |\n| `throwHttpErrors` | `boolean \\| (status) => boolean` | `true` | Throw `HTTPError` on non-2xx responses |\n| `redirect` | `'follow' \\| 'manual' \\| 'error'` | `'follow'` | Redirect mode — overridden to `'manual'` automatically when `cookieJar` is active |\n| `maxRedirects` | `number` | `10` | Maximum redirect hops (applies when `cookieJar` is active) |\n| `proxyUrl` | `string` | — | HTTP/HTTPS proxy URL |\n| `dispatcher` | `Dispatcher` | — | Raw Undici dispatcher (Pool, ProxyAgent, etc.) |\n| `hooks` | `Hooks` | `{}` | Lifecycle hooks object |\n| `onDownloadProgress` | `(progress, chunk) => void` | — | Called during response body streaming |\n| `onUploadProgress` | `(progress, chunk) => void` | — | Called during request body streaming |\n| `parseJson` | `(text) => unknown` | `JSON.parse` | Custom JSON deserializer |\n| `stringifyJson` | `(value) => string` | `JSON.stringify` | Custom JSON serializer |\n| `context` | `Record<string, unknown>` | `{}` | Arbitrary metadata passed through to all hooks |\n| `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation |\n\n### Retry Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `limit` | `number` | `2` | Maximum number of retry attempts |\n| `methods` | `string[]` | `['get','put','head','delete']` | Methods eligible for retry |\n| `statusCodes` | `number[]` | `[408,413,429,500,502,503,504,521,522,524]` | Status codes that trigger a retry |\n| `afterStatusCodes` | `number[]` | `[413,429,503]` | Status codes that trigger `Retry-After` header parsing |\n| `maxRetryAfter` | `number` | `undefined` | Maximum ms to wait when honouring `Retry-After` |\n| `backoffLimit` | `number` | `Infinity` | Maximum ms for any single retry delay |\n| `delay` | `(attempt) => number` | Exponential | Custom delay function |\n| `jitter` | `boolean \\| (delay) => number` | `false` | Randomise delay to reduce thundering-herd |\n| `retryOnTimeout` | `boolean` | `false` | Retry on `TimeoutError` |\n\n### Error Classes\n\n| Class | Description |\n|---|---|\n| `HTTPError` | Thrown on non-2xx responses. Has `.response`, `.request`, `.options`, `.data` |\n| `TimeoutError` | Thrown when the wall-clock timeout expires |\n| `ForceRetryError` | Thrown inside hooks to force an immediate retry |\n| `CircuitBreakerOpenError` | Thrown when the circuit breaker is in OPEN state |\n\n### Type Guards\n\n```typescript\nimport {\n  isRequestError,           // HTTPError | TimeoutError | ForceRetryError | CircuitBreakerOpenError\n  isHTTPError,              // HTTPError\n  isTimeoutError,           // TimeoutError\n  isForceRetryError,        // ForceRetryError\n  isCircuitBreakerOpenError // CircuitBreakerOpenError\n} from '@sptzx/request';\n```\n\n---\n\n## 🙏 Credits\n\n`@sptzx/request` was built by consolidating and extending the work of these open-source projects:\n\n- **[sindresorhus/ky](https://github.com/sindresorhus/ky)** _(MIT © Sindre Sorhus)_ — The architecture, retry logic, hook system, and `ResponsePromise` pattern that form the structural foundation of this library.\n- **[salesforce/tough-cookie](https://github.com/salesforce/tough-cookie)** _(BSD-3-Clause © Salesforce)_ — The RFC 6265 cookie algorithms powering domain matching, path matching, and attribute parsing.\n- **[nfriedly/set-cookie-parser](https://github.com/nfriedly/set-cookie-parser)** _(MIT © Nathaniel Friedman)_ — The `Set-Cookie` header splitting and parsing logic used in `cookie/parser.ts`.\n\n---\n\n## 📜 License\n\n**MIT** — `@sptzx/request` core and Ky-derived portions.\n**BSD-3-Clause** — tough-cookie-derived portions.\n\nSee [LICENSE](./LICENSE) for the full license texts.\n","readmeFilename":"README.md"}