Skip to content

ServerSentEventParser silently drops data: lines whose value contains U+2028/U+2029/U+0085 #1174

Description

@yingwuzhao

ServerSentEventParser silently drops data: lines whose value contains U+2028/U+2029/U+0085

Versions

Reproduced in released 1.3.0, the main branch, and the v2.0.0-M1 tag — the field regex is identical in all three.

Description

ServerSentEventParser matches SSE fields with:

private val Field = """([^:]+): ?(.*)""".r

used via Scala Regex unapply, which requires a full matches(). Java's . (without DOTALL) does not match the Unicode line terminators U+0085 (NEL), U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR). LineParser splits the byte stream only on CR/LF/CRLF, so those characters legitimately remain inside a line. When a data: value contains one, the full-line Field match fails, the line falls through to case _ => // ignore, and no ServerSentEvent is emitted — silently, with no error and no log.

Per the WHATWG SSE spec, an event stream's lines end only on CR/LF/CRLF; U+2028/U+2029/U+0085 are ordinary field content. So a spec-conformant server can legally send them and the client drops the event.

Reproduction

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.scaladsl.unmarshalling.sse.EventStreamParser
import org.apache.pekko.stream.scaladsl.{Sink, Source}
import org.apache.pekko.util.ByteString
import scala.concurrent.Await
import scala.concurrent.duration._

implicit val sys = ActorSystem()
val ls = 0x2028.toChar.toString // U+2028; also fails for 0x2029 and 0x0085
val bytes = ByteString(s"data: before${ls}after\n\n")
val events = Await.result(
  Source.single(bytes).via(EventStreamParser(Int.MaxValue, Int.MaxValue)).runWith(Sink.seq),
  5.seconds)
// events == Vector()  ; expected Vector(ServerSentEvent("before<U+2028>after"))

Suggested fix

Make the value group span line terminators, e.g. add a DOTALL flag:

private val Field = """(?s)([^:]+): ?(.*)""".r

(?s) affects only the . in the value group; [^:]+ (a negated class) is unaffected, so field-name / event: / id: / retry: handling is unchanged.

Impact

Any consumer using EventStreamParser against a server that emits these code points loses events silently. In our case a single U+2028 in a JSON payload stalled cross-region config replication with no error surfaced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions