feat: parse application/x-www-form-urlencoded request bodies#161
feat: parse application/x-www-form-urlencoded request bodies#161cyfung1031 wants to merge 2 commits into
Conversation
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>
There was a problem hiding this comment.
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.
| (_request, body, done) => { | ||
| done(null, Object.fromEntries(new URLSearchParams(body as string))); | ||
| }, |
There was a problem hiding this comment.
Security & Correctness Issue: Prototype Pollution and Safe Parsing
Using Object.fromEntries(new URLSearchParams(body as string)) introduces two issues:
- Prototype Pollution / Shadowing: If a malicious payload contains keys like
__proto__orconstructor, it can shadow standard object properties or lead to prototype pollution if the parsed object is later merged using a vulnerable deep-merge utility. - Unexpected Input Handling: If
bodyis somehowundefinedornullat 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
bodyis a string. - Filter out sensitive keys like
__proto__andconstructorduring 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);
},|
Sorry for using Vibe Coding. In one of my projects (https://github.com/scriptscat/scriptcat/),
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>
|
Thanks for the review. Pushed a fix that extracts the parsing into a standalone
One correction on the diagnosis, for the record: |

Please check if the PR fulfills these requirements
What kind of change does this PR introduce? Feature — parse
application/x-www-form-urlencodedrequest bodies on/post,/put,/patch, and/anything.Summary
Fastify only understands
application/jsonandtext/plainout of the box, so any client that submits a url-encoded form body — a plain HTML form, orGM_xmlhttpRequest/XHRsendingContent-Type: application/x-www-form-urlencoded— currently gets rejected with415 Unsupported Media Typebefore the request ever reaches/post,/put,/patch, or/anything.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 dedicatedformandjsonfields with doc comments describing exactly this intent:so this fills in that gap.
Fix
Adds
registerFormUrlencodedParser()insrc/fastify-config.ts, using only Node's built-inURLSearchParams(no new dependency) — consistent with how querystrings are already parsed into a plain object elsewhere in this repo (/get'squeryParams). It's registered once on the top-level server inMockHttp.start(), so it applies to/post,/put,/patch,/delete, and/anythinguniformly, and doesn't touch the existingapplication/jsonhandling at all.I deliberately left
multipart/form-dataout 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
describe("registerFormUrlencodedParser", ...)intest/fastify-config.test.tscovering the parser in isolation, plus a check thatapplication/jsonbodies are still handled by Fastify's default parser untouched.test/mock-http.test.tsthat starts a realMockHttpserver and posts a form-encoded body to both/postand/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.tsx src/index.tsinstance thatapplication/jsonPOSTs are unaffected.This is independent of #160 (the
maxParamLengthfix) — happy to have them reviewed separately since they touch different concerns, even though both land infastify-config.ts.