Skip to content

feat(routes): support URL-encoded and multipart request bodies - #213

Merged
07prajwal2000 merged 3 commits into
Fluxify-rest:mainfrom
07prajwal2000:feat/request-body-content-types
Aug 7, 2026
Merged

feat(routes): support URL-encoded and multipart request bodies#213
07prajwal2000 merged 3 commits into
Fluxify-rest:mainfrom
07prajwal2000:feat/request-body-content-types

Conversation

@07prajwal2000

Copy link
Copy Markdown
Collaborator

Closes #161

Why

Get Request Body only ever produced JSON, and not reliably:

  • contentType === "application/json" was an exact match, so application/json; charset=utf-8 fell through to .text() and the workflow got a string.
  • application/x-www-form-urlencoded returned a raw FormData object — not JSON-serializable, not usable by body-schema validation, no .field access.
  • multipart/form-data hit .text() and arrived as raw MIME.
  • Nothing capped the body size.

The fix belongs in the transport, not the block: the same value feeds the block, the compiled emitter's ctx.requestBody, the JS-runner global getRequestBody(), and body validation. Fixing only the block would leave the other three broken.

What

Content types are now a per-route setting. A route accepts application/json unless told otherwise; anything else is a 415 with a message naming what it does accept.

Content type Body becomes
application/json the parsed value
application/x-www-form-urlencoded plain object, string values
multipart/form-data plain object; files stay native File
application/octet-stream a Blob
text/plain the string

Repeated keys (tag=a&tag=b, two files under one name) collect into an array — silently dropping the second file of a two-file upload is worse than an uneven shape. POST/PUT only; DELETE may carry a body per RFC 9110, but we don't invite it.

New http_route_config table (route_id PK / project_id / route_config jsonb / timestamps). An open bag rather than a column per setting, so the next per-route knob is a change to lib/routeConfig.ts and not a migration. Patches merge with jsonb || so a key written by a newer build isn't clobbered. Missing or unparseable config falls back to the default — a bad row must never take a route down.

Parsing happens after the route matches. envelopeFromHttp now attaches a bodyReader and dispatch invokes it with the matched route's allowlist, so an unacceptable content type is rejected without reading the body at all. Non-HTTP producers (NATS/BullMQ, the test-suite runner) still supply a plain body and are untouched. Async-reply requests buffer before the 202, since the request stream isn't guaranteed to outlive the response.

Size cap: WORKER_MAX_STREAM_SIZE (kilobytes, default 8192 = 8 MB), applied at Bun.serve on both worker paths and re-checked per request → 413. The execution child process clears its own env, so the value travels in ExecutionBootstrap.

Schema types: file and blob with maxSize / minSize / mimeTypes (an array or a comma-separated string). File[] already works via arr + file items. Mime comparison uses the media type only — text/plain;charset=utf-8 matches a text/plain rule.

Portal: the create-route modal gained an Accepted content types picker (shown for POST/PUT), backed by a new MultiSelect in packages/components — HeroUI's Select in multiple mode with chips in the trigger. The generated OpenAPI spec now lists the route's real content types instead of assuming JSON.

Tests

  • requestBody.spec.ts — 18 cases: charset tolerance, each content type, repeated keys, __proto__ staying an own property, malformed body → 400, oversize by Content-Length and by actual bytes → 413.
  • dispatch.spec.ts — 415 for a disallowed type, the default-JSON fallback, the parsed body reaching the graph, and parsed body → body-schema validation (multipart against file size/mime rules, urlencoded against a string rule).
  • compilerFormData.spec.ts — four compiled graphs run over a parsed form body: a JS runner reading a file's bytes then branching, the empty-upload failure path, a File surviving a setvar round trip by reference, and a foreach over a repeated field.
  • schemaParser.test.ts / routeConfig.test.ts — file/blob rules, and config defaults and fallbacks.

The compiled-graph test caught a real bug before this shipped: Bun reports a text part as text/plain;charset=utf-8, which failed an exact-match text/plain mime rule. Hence the media-type normalization above.

Repo lint green; 658 unit + 78 integration tests pass.

Notes for review

  • Behaviour change for existing deployments. A live route called with x-www-form-urlencoded (previously an unusable FormData) or an arbitrary content type (previously .text()) now gets a 415 until its accepted types are set. Existing routes keep working for JSON, which is what nearly all of them use.
  • Editing is API-only for now. update-partial accepts acceptedContentTypes; there is no route-settings screen yet, so the picker lives in the create modal until the portal settings page lands. routes/update (full PUT) is deliberately untouched.
  • Not covered by tests: the DB round-trip (patchRouteConfig → loader/compiler), which needs a live Postgres, and the MultiSelect in a browser.
  • Files are held in memory and cannot be returned from a Response block. The docs now point at pre-signed object-storage URLs for real uploads — Fluxify is an API server, not an upload gateway.

🤖 Generated with Claude Code

07prajwal2000 and others added 3 commits August 7, 2026 01:01
Get Request Body only ever produced JSON. `application/json; charset=utf-8`
missed the exact-match check and fell through to raw text, urlencoded bodies
came back as an unusable FormData object, and multipart arrived as raw MIME.

Body parsing moves out of the envelope adapter and behind a per-route
allowlist. Each route stores its accepted content types in a new
`http_route_config` table — a jsonb bag keyed by route, so the next per-route
setting is a change to `lib/routeConfig.ts` rather than a migration. Routes
with no row accept JSON, exactly as before.

- JSON, urlencoded, multipart, octet-stream and text/plain are supported;
  anything else is a 415. Repeated form keys collect into an array, and
  multipart files stay native File objects so a JS Runner can read them.
- The body is read only after the route matches, so the allowlist can gate the
  parse instead of auditing bytes already buffered.
- WORKER_MAX_STREAM_SIZE (KB, default 8192) caps a request body at Bun.serve
  and again per request; over it is a 413.
- Route schemas gain `file` and `blob` types with size and mime rules.
- The create-route form picks the accepted types via a new MultiSelect, and
  the generated OpenAPI spec lists them instead of assuming JSON.

Closes Fluxify-rest#161

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
The multipart/urlencoded parser and the file/blob schema types were each
tested alone. These two run the real seam: a multipart request through
dispatch into a bodySchema with file size and mime rules, and a urlencoded
body against a string rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four graphs compiled and executed against a multipart-shaped requestBody:
a JS runner reading a file's bytes and branching on them, the failure path
for an empty upload, a File surviving a setvar round trip by reference, and
a foreach over a repeated form field.

The first of these caught a real bug: a part announced as
'text/plain;charset=utf-8' failed a 'text/plain' mimeTypes rule, because the
schema parser compared the content type verbatim. It now compares the media
type only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@07prajwal2000
07prajwal2000 added this pull request to the merge queue Aug 7, 2026
Merged via the queue into Fluxify-rest:main with commit ff2e7a1 Aug 7, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support URL-encoded and Multipart Form Data in Get Request Body Block

1 participant