npm package: @qezor/reqex
Description: Zero-dependency parsing-first request utilities
Author: Abdul Ahad
Latest version: 2.0.4

# @qezor/reqex

Zero-dependency request parsing utilities for runtimes where resources can be scarce.

`reqex` is intentionally small. It focuses on:
- body source detection
- header normalization and parsing
- cookie parsing
- raw body reading with limits and caching
- body parsing for `json`, `form`, `text`, `binary`, and `multipart`
- streaming multipart iteration for lower-memory upload handling
- true chunked multipart file streaming

It does **not** handle:
- query parsing
- URL routing
- request normalization across framework-specific shapes

Use `@qezor/urlex` for URL and query-oriented utilities.

## Installation

```bash
npm install @qezor/reqex
```

## Import

```js
// CommonJS
const reqex = require("@qezor/reqex")

// ESM
import reqex from "@qezor/reqex"
```

## Why It Exists

Most request helpers either:
- assume one runtime only
- allocate too much for simple parsing
- hide body-consumption behavior
- or mix routing, normalization, and parsing into one package

`reqex` is meant to be a dependable parsing layer that stays small and predictable.

## Resource Model

`reqex` is built with low-resource environments in mind:
- body reads are size-limited
- parsed results are cached per request object when possible
- normalized header maps and parsed header/cookie results may be cached per input object
- non-extensible request objects are cached through internal `WeakMap` storage
- `streamMultipart()` exists so large multipart payloads can be processed incrementally
- `streamMultipartChunks()` exists for true chunk-by-chunk file streaming without buffering entire file parts

Important:
- `parseBody()` is a convenience API
- `streamMultipart()` is a convenience multipart collector
- for large multipart uploads, prefer `streamMultipartChunks()`
- if you fully parse multipart into objects, file bytes still end up buffered because the returned result contains file data

## Supported Body Inputs

`reqex` can read bodies from:
- plain strings
- `Buffer`
- `Uint8Array`
- `ArrayBuffer`
- `Blob`
- `URLSearchParams`
- `FormData`
- Node readable streams
- Web `ReadableStream`
- Fetch-style request objects exposing `arrayBuffer()`, `text()`, or `formData()`
- Lambda-style base64 bodies with `isBase64Encoded: true`
- already parsed plain objects or arrays

## API

### `detectBodySource(input)`

Detects the underlying body carrier.

```js
const source = reqex.detectBodySource({
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ ok: true }),
})

console.log(source.kind) // "string"
```

### `normalizeHeaders(input)`

Lowercases header keys and converts array values into comma-separated strings.

```js
const headers = reqex.normalizeHeaders({
  Host: "example.com",
  Authorization: "Bearer token",
})

// { host: "example.com", authorization: "Bearer token" }
```

### `parseHeaders(input)`

Parses normalized header data and content-type metadata.

```js
const headers = reqex.parseHeaders({
  headers: {
    "content-type": "multipart/form-data; boundary=abc123",
    "content-length": "512",
  },
})

console.log(headers.boundary)      // "abc123"
console.log(headers.contentLength) // 512
console.log(headers.isMultipart)   // true
```

### `parseCookies(input)`

Parses the `cookie` header into an object.

Malformed percent-encoding is preserved instead of throwing.

```js
const cookies = reqex.parseCookies({
  headers: {
    cookie: "session=abc; theme=dark",
  },
})

// { session: "abc", theme: "dark" }
```

### `rawBody(input, options?)`

Reads the request body into a `Buffer`.

Options:
- `limit`: max body size in bytes, default `1048576`

```js
const raw = await reqex.rawBody({
  headers: { "content-type": "text/plain" },
  body: "hello",
})

console.log(raw.toString("utf8")) // "hello"
```

### `parseBody(input, options?)`

Parses the request body into a useful shape.

Supported `options.type` values:
- `"auto"` default
- `"json"`
- `"form"`
- `"multipart"`
- `"text"`
- `"binary"`
- `"pre_parsed"`

```js
const body = await reqex.parseBody({
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ hello: "world" }),
})

// { hello: "world" }
```

`parseBody()` behavior:
- `json` returns parsed objects or arrays
- `form` returns a key-value object, repeated keys become arrays
- `text` returns a string
- `binary` returns a `Buffer`
- `multipart` returns an object where repeated fields become arrays and files are shaped like:

```js
{
  filename: "avatar.png",
  contentType: "image/png",
  size: 1234,
  data: Buffer
}
```

Already-parsed bodies are respected for `json`, `form`, `multipart`, and `pre_parsed`.

### `streamMultipart(input, options?)`

Streams multipart data part-by-part.

This is the lower-memory path for multipart uploads.

Important:
- this still yields fully collected file parts
- it is lower-level than `parseBody()`, but not the true chunked file path

Each yielded part is one of:

```js
// field part
{
  name: "title",
  isFile: false,
  filename: null,
  contentType: "text/plain",
  headers: { ... },
  size: 5,
  value: "Qezor"
}

// file part
{
  name: "asset",
  isFile: true,
  filename: "model.glb",
  contentType: "model/gltf-binary",
  headers: { ... },
  size: 114366,
  data: Buffer
}
```

Example:

```js
for await (const part of reqex.streamMultipart(request, { limit: 5 * 1024 * 1024 })) {
  if (part.isFile) {
    console.log(part.filename, part.size)
  } else {
    console.log(part.name, part.value)
  }
}
```

### `streamMultipartChunks(input, options?)`

Streams multipart data as events, including true file chunks.

This is the best choice when memory matters.

Events:

```js
{ type: "field", name, headers, contentType, size, value }
{ type: "file-start", name, headers, contentType, filename }
{ type: "file-chunk", name, headers, contentType, filename, chunk }
{ type: "file-end", name, headers, contentType, filename, size }
```

Example:

```js
for await (const event of reqex.streamMultipartChunks(request, { limit: 20 * 1024 * 1024 })) {
  if (event.type === "field") {
    console.log("field", event.name, event.value)
    continue
  }

  if (event.type === "file-start") {
    console.log("file start", event.filename)
    continue
  }

  if (event.type === "file-chunk") {
    console.log("chunk bytes", event.chunk.length)
    continue
  }

  if (event.type === "file-end") {
    console.log("file end", event.filename, event.size)
  }
}
```

## Error Model

`reqex` throws `ReqexError` with a stable `code`.

Common codes:
- `BODY_TOO_LARGE`
- `INVALID_JSON`
- `INVALID_BODY_TYPE`
- `NO_MULTIPART_BOUNDARY`
- `INVALID_MULTIPART`
- `INCOMPLETE_MULTIPART`
- `UNSUPPORTED_BODY_SOURCE`
- `UNRAWABLE_BODY_SOURCE`

Example:

```js
try {
  await reqex.parseBody(request, { limit: 1024 })
} catch (error) {
  if (error && error.isReqex) {
    console.log(error.code, error.meta)
  }
}
```

## Integration Notes

For best results:
- call `rawBody()` or `parseBody()` only once per request shape unless you intentionally rely on cache reuse
- treat normalized/parsing results as read-only derived data
- prefer `streamMultipartChunks()` when upload memory pressure matters
- use `streamMultipart()` when you want a simpler part-by-part convenience API
- keep body limits explicit in upload-heavy environments
- do not expect `reqex` to normalize framework-specific request objects for you

## Testing

```bash
npm test
```

## Benchmarking

```bash
npm run bench
```

The benchmark is intentionally local and dependency-free. It is useful for spotting regressions in the hot paths, not for pretending to be a full profiler.

Current suite covers:
- strings, buffers, blobs, streams, base64 bodies
- pre-parsed middleware bodies
- non-extensible web-request objects
- form, json, text, binary, multipart parsing
- multipart chunk-boundary behavior
- malformed cookie input
- invalid `content-length`

## License

MIT © Abdul Ahad