Skip to content

feat: parse application/x-www-form-urlencoded request bodies#161

Closed
cyfung1031 wants to merge 2 commits into
jaredwray:mainfrom
cyfung1031:feat/form-urlencoded-body-parser
Closed

feat: parse application/x-www-form-urlencoded request bodies#161
cyfung1031 wants to merge 2 commits into
jaredwray:mainfrom
cyfung1031:feat/form-urlencoded-body-parser

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown

Please check if the PR fulfills these requirements

  • Followed the Contributing and Code of Conduct guidelines.
  • Tests for the changes have been added (for bug fixes/features) with 100% code coverage.

What kind of change does this PR introduce? Feature — parse application/x-www-form-urlencoded request bodies on /post, /put, /patch, and /anything.


Summary

Fastify only understands application/json and text/plain out of the box, so any client that submits a url-encoded form body — a plain HTML form, or GM_xmlhttpRequest/XHR sending Content-Type: application/x-www-form-urlencoded — currently gets rejected with 415 Unsupported Media Type before the request ever reaches /post, /put, /patch, or /anything.

$ curl -s -X POST -d "a=1&b=two" -H "Content-Type: application/x-www-form-urlencoded" http://127.0.0.1:3000/post
{"statusCode":415,"code":"FST_ERR_CTP_INVALID_MEDIA_TYPE","error":"Unsupported Media Type","message":"Unsupported Media Type"}

I hit this while migrating a project's browser-extension test suite off another httpbin-style service onto mockhttp.org, and had to drop a "POST form-encoded data" test case because it couldn't be exercised against this server at all.

/anything's own response schema already has dedicated form and json fields with doc comments describing exactly this intent:

// If you parse application/x-www-form-urlencoded
// these fields land in request.body too, but you can
// mirror them separately if you like
form: { ... }

so this fills in that gap.

Fix

Adds registerFormUrlencodedParser() in src/fastify-config.ts, using only Node's built-in URLSearchParams (no new dependency) — consistent with how querystrings are already parsed into a plain object elsewhere in this repo (/get's queryParams). It's registered once on the top-level server in MockHttp.start(), so it applies to /post, /put, /patch, /delete, and /anything uniformly, and doesn't touch the existing application/json handling at all.

I deliberately left multipart/form-data out of scope — that needs a new dependency (@fastify/multipart) and is a bigger design decision than this PR should make unilaterally. Happy to follow up with that separately if it's wanted.

Test plan

  • Added describe("registerFormUrlencodedParser", ...) in test/fastify-config.test.ts covering the parser in isolation, plus a check that application/json bodies are still handled by Fastify's default parser untouched.
  • Added an integration test in test/mock-http.test.ts that starts a real MockHttp server and posts a form-encoded body to both /post and /anything, asserting the previously-415 request now returns 200 with the decoded fields.
  • pnpm test (lint + full vitest run with coverage) passes locally: 478 tests, 100% line coverage, no lint warnings.
  • Manually verified against a local tsx src/index.ts instance that application/json POSTs are unaffected.

This is independent of #160 (the maxParamLength fix) — happy to have them reviewed separately since they touch different concerns, even though both land in fastify-config.ts.

Fastify only understands application/json and text/plain out of the
box, so any client that submits a url-encoded form body - a plain
HTML form, or GM_xmlhttpRequest/XHR sending
Content-Type: application/x-www-form-urlencoded - gets rejected with
415 Unsupported Media Type before the request ever reaches /post,
/put, /patch, or /anything.

/anything's own response schema already has dedicated `form` and
`json` fields with doc comments describing this exact intent ("If you
parse application/x-www-form-urlencoded these fields land in
request.body too"), so this fills in that gap using only Node's
built-in URLSearchParams (no new dependency), consistent with how
querystrings are already parsed into a plain object elsewhere in this
repo. It leaves multipart/form-data out of scope since that needs a
new dependency (@fastify/multipart) and is a bigger design decision -
happy to follow up separately if that's wanted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for parsing application/x-www-form-urlencoded request bodies in Fastify, preventing 415 errors for form submissions. Feedback highlights a security vulnerability regarding prototype pollution and potential runtime issues when parsing query parameters directly into an object, suggesting a safer parsing implementation.

Comment thread src/fastify-config.ts
Comment on lines +73 to +75
(_request, body, done) => {
done(null, Object.fromEntries(new URLSearchParams(body as string)));
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Security & Correctness Issue: Prototype Pollution and Safe Parsing

Using Object.fromEntries(new URLSearchParams(body as string)) introduces two issues:

  1. Prototype Pollution / Shadowing: If a malicious payload contains keys like __proto__ or constructor, it can shadow standard object properties or lead to prototype pollution if the parsed object is later merged using a vulnerable deep-merge utility.
  2. Unexpected Input Handling: If body is somehow undefined or null at runtime, new URLSearchParams(body as string) will stringify it to "undefined" or "null", resulting in unexpected parsed keys like {"undefined": ""}.

To prevent these issues, we should:

  • Verify that body is a string.
  • Filter out sensitive keys like __proto__ and constructor during parsing.
		(_request, body, done) => {
			if (typeof body !== "string") {
				done(null, {});
				return;
			}
			const parsed: Record<string, string> = {};
			for (const [key, value] of new URLSearchParams(body)) {
				if (key === "__proto__" || key === "constructor") {
					continue;
				}
				parsed[key] = value;
			}
			done(null, parsed);
		},

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it has been resolved by pushing commit 942b3ec

@cyfung1031

Copy link
Copy Markdown
Author

Sorry for using Vibe Coding.

In one of my projects (https://github.com/scriptscat/scriptcat/),
I am considering to migrate from https://httpbun.com/ to https://mockhttp.org/
However, I found that application/x-www-form-urlencoded request bodies are not supported yet.

Screenshot 2026-07-18 at 7 36 17

Hope you might consider to merge this PR for the API enhancement.

…dowed keys

Per review feedback, extracted the parsing logic into a standalone
parseFormUrlencoded() so its edge cases are directly unit-testable:

- Guards against non-string body input (the content-type parser's
  type signature is `unknown`, even though Fastify's parseAs: "string"
  option guarantees a string in practice).
- Drops __proto__/constructor keys while building the object by hand
  instead of via Object.fromEntries. To be precise about the actual
  risk (the review flagged this as prototype pollution): Object.fromEntries
  does not trigger the Object.prototype.__proto__ setter, so this was
  never an exploitable pollution path - verified directly:

    Object.fromEntries(new URLSearchParams("__proto__=x"))
    // => { ['__proto__']: 'x' }, an own property; the object's actual
    // prototype is untouched and Object.prototype itself is not polluted.

  Filtering these keys is still worth doing though: it stops a form
  field literally named "constructor" or "__proto__" from shadowing
  the real one on the object this server echoes back to API clients.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031

Copy link
Copy Markdown
Author

Thanks for the review. Pushed a fix that extracts the parsing into a standalone parseFormUrlencoded() and:

  • guards against non-string body input
  • drops __proto__/constructor keys while building the object by hand instead of via Object.fromEntries

One correction on the diagnosis, for the record: Object.fromEntries does not trigger the Object.prototype.__proto__ setter, so the original code wasn't an exploitable prototype-pollution path — Object.fromEntries(new URLSearchParams("__proto__=x")) produces { ['__proto__']: 'x' } as an ordinary own property; the resulting object's actual prototype stays Object.prototype, and Object.prototype itself is never touched. I verified this directly before changing anything. That said, filtering those keys is still worth doing — it stops a form field literally named constructor or __proto__ from shadowing the real one on the object this server echoes back to API clients, which is a real (if lower-severity) correctness concern. New unit tests cover both the shadowing fix and the non-string-body guard directly.

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.

2 participants