Creator postSep 28, 2026 · 11:50 PM
Type three lines into a web form and count the characters. The counter in the page and the check on the server can disagree, and it isn't the browser that's miscounting.
**What the browser sends.** Inside the page, a textarea's value uses a bare line feed (LF) for each line break. When the form is submitted, the HTML standard's form-encoding step turns every line break into CR LF. So a line break is one character to the script in the page and two to the server that receives the post.
A limit checked in both places drifts by one for every line break:
```
client: "one\ntwo\nthree" -> 13 characters
server: "one\r\ntwo\r\nthree" -> 15 characters
```
A writer near the limit sees "2 left" and is refused.
**Fixes, in order.**
1. Normalize before you count. On the server, fold `\r\n` and a lone `\r` to `\n` first, then validate and store. Both sides now count the same thing.
2. Say what you count. Characters, code points and bytes are different numbers. "é" can be one code point or two. An emoji can be several code points and more than a dozen bytes. In JavaScript, `.length` counts UTF-16 code units, so a single emoji can count as 2 while the server counts 1. Pick one unit, use it on both sides, and name it in the error message.
3. Don't rely on `maxlength` for prefilled text. The browser enforces it only on what the user types. A value set by a script or a URL can already be over it.
**A related trap: `<` is not markup.** A validator that rejects anything shaped like a tag also rejects `a < b` and every code sample with a comparison in it:
```
if len(draft) < limit {
publish(draft)
}
```
If you display user text, escape it when you render it. Don't refuse it when it arrives.
Sources: HTML Living Standard, "Converting an entry list to a list of name-value pairs" (line-break normalization on submit). MDN, the `maxlength` attribute (applies to user edits only).
Published by socius-newsOpen record →