Skip to content

Commit 2fb9398

Browse files
committed
Skip negative SSE retry spans and render with UInt64
Follow-up to #771: negative retry values are ignored by clients per the SSE spec, so the field is now omitted entirely when the span is negative. With negatives guarded, the retry value renders via .to_u64 so the type reflects that only non-negative milliseconds are emitted.
1 parent 8e10f49 commit 2fb9398

4 files changed

Lines changed: 30 additions & 2 deletions

File tree

.github/skills/kemal-sse/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This skill provides expert guidance on implementing real-time unidirectional eve
1212

1313
- `sse` route helper and `Kemal::EventStream` (named events, `id:`, `retry:`): since Kemal 1.12.0.
1414
- SSE injection protection (CR/LF rejected in `event`/`id`, CRLF normalization in `data` and comments) and the `retry` field Int32 overflow fix: since Kemal 1.13.0.
15+
- Negative `retry` spans are omitted from the output (clients discard non-digit values): unreleased, after Kemal 1.13.0.
1516

1617
## Core Mandates
1718

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- Omit the SSE `retry` field for negative `Time::Span` values instead of emitting e.g. `retry: -3000`, which clients discard per the SSE spec. The value now renders via `.to_u64`; output for non-negative spans is unchanged.
4+
35
- Skip filter tree lookups when no path-scoped filters are registered. Apps using only global filters (`before_all` and friends) no longer pay 4-6 radix lookups and key allocations per request [#781](https://github.com/kemalcr/kemal/pull/781).
46

57
- Cache the `Date` response header string per second instead of formatting it on every request. The value is unchanged: the string is reused only within the same UTC second [#781](https://github.com/kemalcr/kemal/pull/781).

spec/event_stream_spec.cr

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ describe Kemal::EventStream do
3636
client_response.body.should eq("retry: 5184000000\ndata: update\n\n")
3737
end
3838

39+
it "emits a zero retry span" do
40+
sse "/events" do |stream, _|
41+
stream.send("update", retry: 0.seconds)
42+
end
43+
44+
request = HTTP::Request.new("GET", "/events")
45+
client_response = call_request_on_app(request)
46+
client_response.body.should eq("retry: 0\ndata: update\n\n")
47+
end
48+
49+
it "omits negative retry spans" do
50+
sse "/events" do |stream, _|
51+
stream.send("update", retry: -3.seconds)
52+
end
53+
54+
request = HTTP::Request.new("GET", "/events")
55+
client_response = call_request_on_app(request)
56+
client_response.body.should eq("data: update\n\n")
57+
end
58+
3959
it "splits multi-line data into separate data fields" do
4060
sse "/events" do |stream, _|
4161
stream.send("line one\nline two")

src/kemal/event_stream.cr

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ module Kemal
1919

2020
# Sends an SSE event. Multi-line *data* is split into separate `data:` fields.
2121
# *event* and *id* must not contain CR/LF; newlines there raise `ArgumentError`
22-
# so they cannot inject SSE fields.
22+
# so they cannot inject SSE fields. A negative *retry* span is omitted from the
23+
# output, since clients discard non-digit `retry` values anyway.
2324
def send(data : String, *, event : String? = nil, id : String | Int? = nil, retry : Time::Span? = nil) : self
2425
if event
2526
validate_single_line!("event", event)
@@ -30,7 +31,11 @@ module Kemal
3031
validate_single_line!("id", id_value)
3132
@response.puts "id: #{id_value}"
3233
end
33-
@response.puts "retry: #{retry.total_milliseconds.to_i64}" if retry
34+
# Clients discard non-digit retry values (WHATWG HTML), so negative spans are
35+
# skipped. The guard also protects to_u64 below, which raises on negatives.
36+
if retry && !retry.negative?
37+
@response.puts "retry: #{retry.total_milliseconds.to_u64}"
38+
end
3439
each_sse_line(data) do |line|
3540
@response.puts "data: #{line}"
3641
end

0 commit comments

Comments
 (0)