Every string from the outside needs a maximum length
Here’s a finding I’ve written on more code reviews than any other: a string field that comes from a user or a URL, has no maximum length, and lands in an unbounded TEXT column. It’s boring. It’s also a real availability and storage risk, especially on an endpoint that doesn’t require authentication. Someone can hand you a ten-megabyte “name” and you’ll dutifully try to store it.
So you add a max length. Easy. Except the fix has a sharp edge that’s easy to cut yourself on.
The reject-the-whole-payload trap
A lot of validation layers, when any single field fails, reject the entire request. That’s usually what you want. But it means a strict constraint on a minor field can block an otherwise-valid operation. Picture an optional field that gets populated from some upstream system you don’t control. One day that system sends a value a little longer than your cap. Now the whole submission bounces — not because of anything the user did, and with nothing the user can do to fix it.
So before adding a constraint, I trace one thing: how does the validation pipe handle a single field failure? Reject-whole-payload, or per-field? That answer decides everything downstream.
The rule I landed on
Split fields by who controls them.
- Fields the user controls and can correct (their own input): a hard max is fine. If it’s too long, reject it and tell them. They can shorten it.
- Optional fields from untrusted, non-user-editable sources (something forwarded from another system): don’t hard-reject. Truncate, coerce, or drop the bad value. One malformed field from upstream should never sink an entire legitimate request the user has no way to repair.
Hard failures belong on the things a human on the other end can actually act on. Everywhere else, prefer a non-rejecting transform.
Check for a guard you already have
The other reflex worth building: before adding a per-field length check, look for a global one. If there’s already a body-size limit or a gateway capping request size, the catastrophic case — the ten-megabyte blob — is already handled. Adding a redundant field-level constraint on top of that doesn’t buy you protection; it just adds a new way for valid requests to fail. I’ve removed as many pointless constraints as I’ve added meaningful ones.
Apply it uniformly
When I do add bounds, I add them to every field of the same category, not just the one that showed up in the bug report. A schema where three of five user-supplied strings have a max and two don’t isn’t half-secure; it’s inconsistent, and the next person will copy whichever example they land on first. Consistency is the actual deliverable.
Make the fixture prove it
One last gotcha, because it’s cost me twice. If you write a test to assert the cap works, populate the field with a defined value that actually violates it. Equality matchers happily ignore undefined properties, so a fixture that forgot to set the field will pass a test that was supposed to fail. Assert the real, over-length value gets rejected (or truncated), not the absence of a value.
None of this is hard. It’s a five-line change. The discipline is in the five minutes of thinking before the five-line change: who controls this field, what already guards it, and what happens to a valid request when this fails.