Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Align Client SSE support to HTML specification #3312

Merged
merged 11 commits into from
Mar 5, 2025

Conversation

disordered
Copy link
Contributor

@disordered disordered commented Feb 17, 2025

This should (mostly) align SSE support on the Client to HTML specification.

Fixes:

  • Use UTF-8 for encoding (server) and decoding
  • Ignore comment and non-field data (usually used for keep-alive)
  • Ignore non-integer retry values
  • Combine successive data field values
  • Parse events with fields specified in any order
  • Do not emit events without data
  • Do not emit incomplete events
  • Allow field overrides within event
  • Support field names without colon
  • Handling of first space after field name

Breaking:

  • retry is now Duration parsed as milliseconds

Handling of retry

HTML specification leaves a lot of room to interpretation for edge cases on how to dispatch events. For browsers it says to not emit events if data is not specified, however, chromium based browsers consume such events and adjust their retry frequency accordingly. So, this implementation emits such events too with data = "". It is not obvious, however, that non-string codec has to handle this case.

Handling of various line endings

Chromium browsers and Postman handle all versions (Windows, Linux, OSX) of line endings. However, the specification explicitly states only \n should be treated as separator. I considered aligning with the browser implementation, but ZPipeline.splitLines only supports Windows and Linux line endings. It could probably be achieved through some sort of chunk and String.linesIterator mapping.

@CLAassistant
Copy link

CLAassistant commented Feb 17, 2025

CLA assistant check
All committers have signed the CLA.


override def spec: Spec[TestEnvironment with Scope, Any] =
suite("ServerSentEventClientSpec")(
test("Line breaks only payload does not emit events") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postman will emit empty events, chromium does not. Specification says not to.

|""".stripMargin
assertEvents(events, Chunk.empty)
},
test("Successive line breaks do not emit extra events") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the other test, Postman inserts empty events.

}.mkString("", "\n\n", "\n\n")
assertEvents(events, Chunk.fill(3)(ServerSentEvent(data = "data")))
},
test("Event type without data is not emitted") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postman emits the event, Chromium does not. Specification says not to emit.

|""".stripMargin
assertEvents(events, Chunk.single(ServerSentEvent(data = "", retry = Some(1.millisecond))))
},
test("Event retry that is not an integer is not emitted") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postman will emit events with provided retry value. Browser ignores them. Specification says to ignore.

@disordered disordered marked this pull request as ready for review February 19, 2025 16:24
Copy link
Contributor

@987Nabil 987Nabil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small changes

@@ -42,12 +46,12 @@ final case class ServerSentEvent[T](
data: T,
eventType: Option[String] = None,
id: Option[String] = None,
retry: Option[Int] = None,
retry: Option[Duration] = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a constructor, that takes Option[Int] for compatibility.
Also adjust the docs of the param here.

Copy link
Contributor Author

@disordered disordered Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it, though, I realised I missed an edge case, when a negative int value is passed. So, this constructor ends up silently filtering it out as well.

EDIT: Removed again.
The compiler cannot decide which constructor to use when optional values are not provided, e.g. ServerSentEvent("some data"). Named constructor would work, but doesn't address backwards compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add back the constructor with the Int, but without default values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

) {

def encode(implicit binaryCodec: BinaryCodec[T]): String = {
val sb = new StringBuilder
binaryCodec.encode(data).asString.linesIterator.foreach { line =>
binaryCodec.encode(data).asString(StandardCharsets.UTF_8).linesIterator.foreach { line =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use zio.http.Charsets

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

@disordered disordered force-pushed the align-sse-to-html-spec branch 2 times, most recently from ad9f59c to 119d15b Compare March 4, 2025 17:33
@disordered disordered force-pushed the align-sse-to-html-spec branch from 119d15b to f818490 Compare March 4, 2025 17:38
@987Nabil 987Nabil merged commit 06af1a7 into zio:main Mar 5, 2025
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants