feat: add a form-urlencoded decoder with bracketed nesting - #24
Merged
Conversation
|
Warning Review limit reachedNext included review available in 20 minutes. View limit detailsLimit details: You’ve used the included review currently available. Your 93 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Comment |
hughgrigg
force-pushed
the
feat/form-decoder
branch
from
September 2, 2026 08:52
0eb3a09 to
96a1a4b
Compare
`application/x-www-form-urlencoded` is a wire format, and not one service's dialect. Stripe, Rails and PHP applications all speak it, all three write nesting the same way, and none of them could be simulated without a decoder for it. It belongs beside JSON for the same reason JSON is here. `decodeForm` reads `line_items[0][price_data][unit_amount]=250` into nested objects and arrays. Every leaf stays a string: a form body carries no types, and guessing at them would make `quantity=1` and `postcode=01234` disagree about what a digit is. Two decisions worth stating, both taken to keep the parser a page long. Digits order the entries and do not position them, so `a[0]`, `a[5]` and `a[9]` give three elements and never a sparse array of ten. A real encoder counts from zero, where the two readings agree, and this needs no cap on how large an index a hostile body may claim. Four inputs throw a `SyntaxError` rather than being guessed at: a key given twice, a key needing one part to be both a value and a branch, an empty bracket anywhere but the end, and a key brackets cannot be read out of. That follows the existing rule that decode failures stay loud, and it means a simulation can never be quietly corrupted by input a real encoder would never emit. A `__proto__` key lands as an own data property and reaches no prototype, which `Object.fromEntries` gives for free. There is a test pinning it.
hughgrigg
force-pushed
the
feat/form-decoder
branch
from
September 2, 2026 09:28
96a1a4b to
17d1da9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #23, which adds the
decodehook this fills in.application/x-www-form-urlencodedis a wire format, and not one service's dialect. Stripe, Railsand PHP applications all speak it, all three write nesting the same way, and none of them could be
simulated without a decoder for it. It belongs beside JSON for the same reason JSON is here.
{ "line_items": [{ "price_data": { "unit_amount": "250" }, "quantity": "1" }], "expand": ["customer"] }Decisions
Every leaf stays a string. A form body carries no types, and guessing at them would make
quantity=1andpostcode=01234disagree about what a digit is. The resource's own creationbehaviour converts what it needs, where the target shape is known.
Digits order the entries and do not position them.
a[0],a[5]anda[9]give three elementsand never a sparse array of ten. A real encoder counts from zero, where the two readings agree, and
this needs no cap on how large an index a hostile body may claim.
Four inputs throw a
SyntaxErrorrather than being guessed at.name=a&name=ba=1&a[b]=2awould hold a value and more keys at oncea[][b]=1a[b,[a]That follows the existing rule that decode failures stay loud. A real encoder emits well-formed
keys, so these are hostile or mistaken input, and a simulation quietly corrupted by them would be
worse than one that stops.
The
content-typeheader is ignored. What a body claims to be and what it holds are two facts,and choosing the decoder by hand has already settled the first.
Notes
A
__proto__key lands as an own data property and reaches no prototype, whichObject.fromEntriesgives for free. There is a test pinning it.
The parser is a page of code and should stay one. Sparse arrays, dotted keys and
qscompatibilityflags are each a decision a reader would have to hold in their head, and none is needed to simulate
an API.