honojs/hono · error · Error
${key} must not contain "\r" or "\n"
Error message
${key} must not contain "\r" or "\n" What it means
When serializing a Server-Sent Event, Hono validates that the `event` and `id` fields contain no CR or LF characters, because those would break the SSE wire format (each field must be a single `field: value` line). Violating input is rejected with this Error rather than emitting a corrupt stream.
Source
Thrown at src/helper/streaming/sse.ts:30
export class SSEStreamingApi extends StreamingApi {
constructor(writable: WritableStream, readable: ReadableStream) {
super(writable, readable)
}
async writeSSE(message: SSEMessage) {
const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {})
const dataLines = (data as string)
.split(/\r\n|\r|\n/)
.map((line) => {
return `data: ${line}`
})
.join('\n')
for (const key of ['event', 'id'] as const) {
const value = message[key]
if (value && /[\r\n]/.test(value)) {
throw new Error(`${key} must not contain "\\r" or "\\n"`)
}
}
const sseData =
[
message.event && `event: ${message.event}`,
dataLines,
message.id !== undefined && `id: ${message.id}`,
message.retry !== undefined && `retry: ${message.retry}`,
]
.filter(Boolean)
.join('\n') + '\n\n'
await this.write(sseData)
}
}
const run = async (View on GitHub (pinned to e2740d5a1b)
Solutions
- Keep `event` and `id` as short single-line tokens: 'message', 'update', a counter or hash-based id
- Put arbitrary/multi-line content in the `data` field, which SSE encoding handles correctly
- Sanitize before sending: `String(v).replace(/[\r\n]+/g, ' ')` on event/id
- Add a lint/unit assertion that generated event names match /^[^\r\n]*$/
Example fix
// before
await sse.sse({
event: JSON.stringify(payload), // contains newlines -> throws
data: 'x',
})
// after
await sse.sse({
event: 'update',
id: String(seq),
data: JSON.stringify(payload),
}) Defensive patterns
Strategy: validation
Validate before calling
const cleanField = (v: string) => v.replace(/[\r\n]+/g, ' ')
await sse.sse({
event: cleanField(name).slice(0, 64),
id: cleanField(id),
data: JSON.stringify(payload),
}) Type guard
const isSSEFieldSafe = (v: unknown): v is string => typeof v === 'string' && !/[\r\n]/.test(v)
Try / catch
try { await sse.sse(msg) } catch (e) { if (e instanceof Error && /must not contain/.test(e.message)) { await sse.sse({ ...msg, event: 'message', id: undefined }) } else throw e } Prevention
- Only put free-form content in `data`; keep event/id single-line tokens
- Sanitize any user-derived value used as event or id
- Unit-test SSE emitters with multi-line payloads
When it happens
Trigger: Passing `new SSEStreamingApi().sse({ event: ..., id: ... })` / streaming SSE messages where the `event` name or `id` string contains '\n', '\r', or a multi-line value (e.g. using a full message or JSON blob as the event name or id).
Common situations: Using the whole error message (which may be multi-line) as the SSE event name; using timestamps+uuid ids built from template strings that accidentally include newlines; forwarding untrusted user input into event/id; copy-pasting examples that put data in `event`.
Related errors
- Invalid JSX tag name: ${tag}
- Invalid rule: ${rule}
- verifyWithJwks requires options for either "keys" or "jwks_u
- Malformed JSON in request body
- ERR_INVALID_IP_ADDRESS
AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28).
Data as JSON: /api/errors/bb5a59fa87554cdc.
Report an issue: GitHub.