denoland/deno · error · TypeError
ERR_INVALID_CHAR
ERR_INVALID_CHAR
Error message
Invalid character in header content ["Link"]
What it means
ERR_INVALID_CHAR thrown while composing an Early Hints response: the link hint is normalized by validateLinkHeaderValue(), then re-checked with checkInvalidHeaderChar(). If the resulting value still contains CR, LF, or other characters illegal in a header, the write is refused with 'Invalid character in header content ["Link"]' — this is the response-splitting defense for the Link header.
Solutions
- Strip CR/LF and control characters from every dynamic segment before building the Link value
- Restrict dynamic parts to an allowlist pattern such as /^[\w.-]+$/ for filenames
- Encode or reject unsafe characters instead of passing them through
Example fix
// before
res.writeEarlyHints({ link: `</assets/${req.query.css}>; rel=preload` }); // css contains %0d%0a
// after
const css = String(req.query.css).replace(/[\r\n\u0000-\u001f]/g, '');
res.writeEarlyHints({ link: `</assets/${css}>; rel=preload` }); Defensive patterns
Strategy: validation
Validate before calling
const sanitizeHeaderValue = (s) => String(s).replace(/[\r\n\u0000-\u001f\u007f]/g, '');
Type guard
const isSafeHeaderValue = (s) => !/[\r\n\u0000-\u001f\u007f]/.test(String(s));
Try / catch
try {
res.writeEarlyHints({ link });
} catch (e) {
if (e.code === 'ERR_INVALID_CHAR') return; // skip unsafe hint, keep serving
throw e;
} Prevention
- Treat any string flowing into a header as untrusted
- Build Link values from constants plus strictly validated dynamic parts
- Add a shared sanitize step for every header you compose from user input
When it happens
Trigger: res.writeEarlyHints({ link: `</assets/${req.query.file}>; rel=preload` }) where the interpolated value contains %0d%0a (decoded to CR LF), a raw newline from config, or other control bytes.
Common situations: Interpolating request parameters or config values into preload hints; Link values copied from multi-line YAML strings; penetration tests probing header injection through Early Hints.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ERR_INVALID_CHAR
- ERR_HTTP_HEADERS_SENT
- ERR_INVALID_HTTP_TOKEN
- ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH
- ERR_HTTP_BODY_NOT_ALLOWED
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/b0eb802bf3b17fb6.
Report an issue: GitHub.
Appendix: source
Thrown at ext/node/polyfills/_http_server.js:473
hints,
cb,
) {
let head = "HTTP/1.1 103 Early Hints\r\n";
validateObject(hints, "hints");
if (hints.link === null || hints.link === undefined) {
return;
}
const link = validateLinkHeaderValue(hints.link);
if (link.length === 0) {
return;
}
if (checkInvalidHeaderChar(link)) {
throw new ERR_INVALID_CHAR("header content", "Link");
}
head += "Link: " + link + "\r\n";
const keys = ObjectKeys(hints);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key !== "link") {
validateHeaderName(key);
const value = hints[key];
validateHeaderValue(key, value);
head += key + ": " + value + "\r\n";
}
}
head += "\r\n";
this._writeRaw(head, "ascii", cb);View on GitHub (pinned to 9ad36f7a2c)