deadhead
head/charset-position

charset declared after the first 1024 bytes

The encoding declaration must be fully inside the first 1024 bytes or the parser never sees it.

  • Avoid
  • Harmful
Severity
HarmfulActively breaks something for users.
Basis
SpecifiedThe evidence is in a current standard.
Detectable
YesMatched exactly. Autofixable when the rule carries a fix.
Matches
logic moduleDocument rule, inside <head>.
Fix
None — reports only
Impacts
Interoperability and Security
Related
meta/http-equiv-x-ua-compatible

Before a browser can parse a document it has to decide how to decode the bytes. With no HTTP charset parameter and no byte order mark, it runs a prescan: it reads the start of the byte stream looking for an encoding declaration, and the HTML Standard caps that prescan at 1024 bytes. An encoding declaration that is not completely serialized inside that window does not exist as far as the parser is concerned.

Why avoid

Past the window, the browser falls back to a locale-dependent guess and may then discover the real declaration mid-parse — at which point it has to throw away the tree and reparse the document from the beginning. The visible failure is mojibake: café rendered as café for some visitors and correctly for others, depending on their locale, which is exactly the kind of bug that does not reproduce on the developer’s machine.

It is a security boundary as well as a correctness one. When the encoding is guessed rather than declared, an attacker who controls part of the page can influence how the rest of it is decoded, which is the mechanism behind the classic UTF-7 XSS: markup that is inert under UTF-8 becomes active script under a guessed encoding.

The rule is easy to trip without noticing. A long <title>, a block of Open Graph tags, an inline critical-CSS <style>, or a comment banner ahead of the declaration will each push it past 1024 bytes, and nothing warns you.

Use instead

Put the declaration first, before anything else in <head>:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>…</title>
  </head>
</html>

Sending Content-Type: text/html; charset=utf-8 on the response is stronger still — it is authoritative and needs no prescan — but keep the <meta> too, so the file stays correct when it is opened from disk or served by something you do not control.

Detectability

Detectable wherever the original source text is available, which is why this is a kind: "document" rule: no selector can express “ends after byte 1024”. The logic compares the declaration’s end offset against the limit, so it reports in the CLI and the ESLint plugin but stays silent in the browser adapter, where a live DOM node has no source offsets to compare.

Two deliberate limits. It measures offsets in characters rather than bytes, and any non-ASCII byte ahead of the declaration only makes the true count larger — so the check can under-report but never invents a finding. And it looks at <meta charset> only: an http-equiv="Content-Type" declaration is bound by the same 1024 bytes, but deciding whether one is a valid declaration means parsing its content, and that is a separate rule.

Resources

This page is generated from content/rules/head/charset-position.md, the same file the linter is built from. Think the rule is wrong, or that browsers moved on? Say so — that is the most useful issue you can file.