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

  1. Keep `event` and `id` as short single-line tokens: 'message', 'update', a counter or hash-based id
  2. Put arbitrary/multi-line content in the `data` field, which SSE encoding handles correctly
  3. Sanitize before sending: `String(v).replace(/[\r\n]+/g, ' ')` on event/id
  4. 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

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


AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28). Data as JSON: /api/errors/bb5a59fa87554cdc. Report an issue: GitHub.