tekir
All Packages
Storage & Parsingv0.1.8

@tekir/bodyparser

Body parsing: JSON, form, multipart, raw. File uploads with validation.

Installation

$bun add @tekir/bodyparser

Features

  • JSON, URL-encoded form, and raw body parsing
  • Multipart file uploads with parseMultipart()
  • UploadedFile with size and strict whitelist content validation
  • AdonisJS-style accessors: `ctx.file(name)`, `ctx.files(name)`, `ctx.allFiles()`
  • Always-present helpers: non-multipart requests get a no-op fallback so handlers do not need optional-chains
  • Configurable size limits per content type
  • BodyParserProvider for automatic registration

Quick Example

TypeScript
import { bodyParser } from '@tekir/bodyparser'

app.router.use(bodyParser())

app.router.post('/upload', async (ctx) => {
  const avatar = ctx.file('avatar', { size: '2mb', extnames: ['jpg', 'png'] })
  if (avatar) await avatar.moveTo('./uploads')

  // Multi-file field
  const photos = ctx.files('photos')
  for (const p of photos) await p.moveTo('./uploads/photos')

  // Or every uploaded file across all fields
  const all = ctx.allFiles()
})

Changelog

v0.1.8LatestSeptember 16, 2026
  • Spilled uploads now load through Node-compatible ESM imports instead of a CommonJS-only runtime require.
  • Published output now uses the shared Node-targeted ESM bundle pipeline with external dependencies and generated TypeScript declarations, while Bun consumers keep the native source export.
v0.1.7July 23, 2026
  • Multipart parsing now enforces a configurable maxParts ceiling in both streaming and fallback paths, rejecting excessive field-and-file payloads with 413 before they can exhaust parser resources.
v0.1.5June 13, 2026
  • Multipart parsing is now streaming with limits applied as the body is read. maxFileSize, total limit, maxFiles (default 20), maxFields (default 1000), and maxParts (default 1000) are enforced during the read and abort early with a 413 once exceeded, so a large upload is no longer buffered fully into memory. Parts over spillThreshold (default 1 MB) stream to a temp file to keep memory bounded.
  • JSON, form, and raw bodies are also size-limited during the read (with a Content-Length pre-check) and cancel as soon as the limit is exceeded.
  • JSON parsing now strips __proto__, constructor, and prototype keys recursively, consistent with the urlencoded path, closing a prototype pollution vector.
  • SVG content detection is hardened so that crafted XML/HTML no longer passes as an image; the docs note that SVG is an active document and should not be added to an extnames whitelist without sanitization. Method spoofing is now opt-in via methodSpoofing: true and only upgrades real POST requests (a GET can never be mutated).
  • Found and fixed with Fable.
v0.1.4May 17, 2026
  • Multipart uploads now reject oversize requests before parsing. When the incoming Content-Length exceeds the configured limit, parseMultipart() throws a new PayloadTooLargeError (HTTP 413) instead of buffering the whole payload into the runtime's FormData parser first. Apps that catch framework errors get a clean 413 path; apps that don't were previously paying memory cost for the rejection.
  • A user-supplied tmpFileName() callback can no longer write outside tmpDir. The returned name is basename()-stripped and the final path is verified to stay under the configured directory; paths like ../../etc/passwd are rejected and surface as a tmpFileName validation error on the affected upload rather than escaping containment.
  • PayloadTooLargeError is exported from @tekir/bodyparser for callers that want to branch on it or attach a custom error handler.
v0.1.3May 4, 2026
  • **Security**: UploadedFile.validateContent() now treats unrecognized magic bytes as outside the extnames whitelist instead of silently allowing them. Previously a renamed malware.exe → malware.jpg slipped through because detectExtname() returned null and the mismatch check was guarded behind if (detected && ...), leaving hasErrors false so the file reached storage. Strict whitelist semantics now: empty buffer → rule: 'content', message: 'Empty file'; magic bytes don't match any known signature → rule: 'content', message: 'Unrecognized file content'; detected format is not in the whitelist → rule: 'content', message: 'File content (.X) is not in allowed types: ...'; declared extension does not match the detected format → rule: 'extname'. The non-strict path (no extnames option) is unchanged.
v0.1.2May 4, 2026
  • **Breaking**: ctx.files is now a method, not a MultipartFiles collection. Multi-file fields are read with ctx.files(name) (returns UploadedFile[]) instead of ctx.files.files(name). Matches AdonisJS' single-method-per-shape pattern (ctx.file() / ctx.files() / ctx.allFiles()) and removes the awkward files.files double-dot. The MultipartFiles class is still exported for advanced use; the parser still produces it internally.
  • All three accessors are installed on ctx for every request, including non-multipart ones, with a no-op fallback (ctx.file() → undefined, ctx.files() → [], ctx.allFiles() → []). Removes the optional-chain dance from controllers, so const avatar = ctx.file('avatar') works in any handler regardless of content-type.
  • ctx.file(name) now returns UploadedFile | undefined (was ... | null) so the entire surface lines up on undefined for the absent case.

Other Storage & Parsing packages