In a web framework's request object, how do the accessors for path variables, query parameters and headers differ?
answer
- three sources, three guarantees
- one comes from the router
- client-supplied values may repeat
- single-value accessor hides repeats
- header names ignore case
basics
~20 sPath variables come from the matched route template, so they exist whenever the handler runs. Query parameters and headers come from the client, so either may be missing or repeated, which is why frameworks expose both single- and multi-value accessors.
solid answer
~40 sThree sources, three guarantees. A **path variable** is produced by the router when the target matched a template such as `/orders/{id}`, so inside the handler it is present by construction; if it were missing this route would not have matched. **Query parameters** and **header fields** are client input: either may be absent, and either may legitimately arrive more than once, so frameworks offer a single-value accessor that collapses repeats and a list accessor that returns them all. Header names are matched without regard to case because the protocol defines them that way, while query keys are compared exactly. All three hand back text — converting it to a number, applying a default or rejecting junk belongs to the binding and validation layer, not to the request object.
go deeper
Recall where each value comes from: the router fills path variables, the client sends query parameters and header fields. Everything arrives as text, and anything the client supplies may be missing.
Explain why single-value and multi-value accessors both exist, what the single-value form does when a key repeats, and why header names are matched without regard to case while query keys are not.
Show that every client-supplied accessor returns untrusted text. Decide up front what absent, empty and repeated each mean for the endpoint, and reject ambiguity rather than silently taking whichever occurrence comes back.
Push for one house convention across services for repeats, absence and empty values, so that behaviour is not re-invented in every handler and cannot differ between two components reading the same request.
## The request object is a view over one parsed message A server-side web framework parses an incoming request once, then hands the handler an object that exposes what that message contained: the method and target, the header fields, the query string, facts about the connection, and the payload. The object does not fetch anything and does not call the client back — it is a **read view over an already-parsed message**. Path variables are the one part that does not come from the message directly: the **router** produces them by matching the request target against a route template such as `/orders/{id}`. That difference in origin is what makes the three families of accessor behave differently, and most everyday bugs in this area come from treating them as interchangeable string lookups. ## Three sources, three guarantees | What you read | Produced by | Present when the handler runs | Can repeat | Name matching | |---|---|---|---|---| | Path variable | the router, from the matched template | yes, for every segment the template requires | no | exact; the name is one you wrote | | Query parameter | the client, from the query string | only if the client sent it | yes | exact, case-sensitive | | Header field | the client, from the header block | only if the client sent it | yes | case-insensitive | The consequences follow directly from the table: - A **path variable** exists because the route matched. If the segment were missing, this handler would not have been selected at all, so defensive "what if it is absent" code is usually noise. The exception is a template that declares a segment as optional or a catch-all — there the framework really can hand you nothing, and you must say what that means. - A **query parameter** is optional by nature. The accessor is a lookup into a map the *client* populated, so absence is ordinary input, not an error, and the framework leaves the policy to you. - A **header field** is also client input, but its name is compared without regard to case, because the protocol defines field names that way. Code that compares names itself must lower-case both sides or it will miss values that were sent with different capitalisation. ## Why the same key has two accessors Both the query string and the header block may legally carry the same name more than once. Frameworks therefore expose two shapes: 1. A **single-value accessor** that returns one entry, collapsing any repeats. Which entry it keeps is not universal — most frameworks return the first occurrence, some return the last. 2. A **multi-value accessor** that returns every occurrence in order. The single-value form is the convenient one and is what most code calls, which is exactly why repeats are a security-relevant subtlety: if one component in the request path reads the first occurrence and another reads the last, the two disagree about what was requested, and a check can be passed on one value while a different value is acted upon. For a key your endpoint expects at most once, the honest pattern is to read the list and reject anything longer than one entry with a `400`, rather than silently taking whichever the framework happens to hand back. ## Absent, empty and blank are three different things A key sent with no value (`?flag=`) is present with an empty value. A key not sent at all is absent. Some accessors flatten both into the same empty result, which is the source of a whole family of "the default did not apply" bugs. If the difference matters — an empty string that should clear a field versus an omitted field that should be left alone — use the accessor that can distinguish them, or read the multi-value form, where absence is an empty list and an empty value is a list of one. ## Everything arrives as text, and none of it is trusted Every one of these accessors returns characters the client chose, in a length the client chose. Two rules follow: - **Conversion is somebody else's job.** Turning the characters `42` into a number, applying a default, or rejecting a value outside a range belongs to the layer that binds and validates input, not to the request object, which is deliberately dumb about meaning. - **Returned is not validated.** The framework confirmed the bytes parsed as a request; it made no claim that the value is sane, in range, or non-hostile. A value read here and interpolated into a query, a file path, a redirect target or a log line carries whatever the caller wrote. ## What to settle before writing the handler For every client-supplied value the endpoint reads, decide three things once and write them down: 1. **Required or optional** — and if optional, what the default is. 2. **Repeat policy** — first, last, all, or reject. 3. **Empty policy** — whether an empty value means the same as absent. Those three decisions turn a pile of ad-hoc string lookups into a stated contract, and they are what a senior answer to this question actually contains.
- A query key arrives twice and the code uses the single-value accessor. What does it get, and why does that matter?It gets one occurrence — most frameworks keep the first, some the last — and the other disappears without a warning. That is exploitable when one component reads the first and another the last, because the check and the action then apply to different values. For a key that should appear once, read the list and reject a size above one.
- Why does an absent query parameter come back as an empty result instead of raising an error?Because absence is ordinary client input, not a programming mistake — the accessor is a lookup into a map the caller populated. The framework has no way to know whether the endpoint wants a default, an empty value or a rejection, so it reports what arrived and leaves the contract to the code that owns the endpoint.
- Where should the conversion from text to a typed value happen, if not in the request object?In the binding and validation layer that runs between the request object and the handler. Keeping the request object a dumb view of what arrived means one place decides types, defaults, ranges and error responses, instead of every handler re-implementing them around raw accessors.
saying these in an interview costs you the question
- Treats a repeated query key as impossible and only ever reads one value
- Compares header names case-sensitively and misses a differently-cased field
- Expects the accessor to hand back numbers or dates rather than text
- Cannot tell an absent parameter from one sent with an empty value
- Treats values as validated simply because the framework returned them