Skip to content

Commit 09a1904

Browse files
xerialclaude
andauthored
Add Response.withEvents for SSE support (#4120)
## Summary Add a public method to set Server-Sent Events on HTTP Response, enabling endpoints to dynamically choose between JSON responses and SSE streaming. ## Changes - Add `Response.withEvents(Rx[ServerSentEvent])` method that: - Sets the SSE events stream on the response - Automatically sets `Content-Type: text/event-stream` if not already set - Preserves existing event-stream content type if already specified - Add `HttpHeader.MediaType.TextEventStream` constant - Add unit tests for the new functionality ## Use Case This enables building API endpoints that can return either: - A standard JSON response (non-streaming) - An SSE stream (streaming) Based on request parameters, for example: ```scala def messages(request: MessagesRequest): Response = { if (request.stream.getOrElse(false)) { val events: Rx[ServerSentEvent] = streamingService.stream(request) Http.response().withEvents(events) } else { val response = blockingService.complete(request) Http.response().withJson(response.toJson) } } ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72faff3 commit 09a1904

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

airframe-http/.jvm/src/test/scala/wvlet/airframe/http/HttpMessageTest.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,33 @@ class HttpMessageTest extends AirSpec {
170170

171171
req1.message.contentHash shouldBe req2.message.contentHash
172172
}
173+
174+
test("create SSE response with withEvents") {
175+
import wvlet.airframe.rx.Rx
176+
177+
val events = Rx.sequence(
178+
ServerSentEvent(data = "event1"),
179+
ServerSentEvent(data = "event2"),
180+
ServerSentEvent(data = "event3")
181+
)
182+
183+
val response = Http.response().withEvents(events)
184+
185+
// Content-Type should be automatically set
186+
response.isContentTypeEventStream shouldBe true
187+
response.contentType shouldBe Some(HttpHeader.MediaType.TextEventStream)
188+
189+
// Events should be accessible
190+
response.events.toSeq.size shouldBe 3
191+
}
192+
193+
test("withEvents preserves existing event-stream content type") {
194+
import wvlet.airframe.rx.Rx
195+
196+
val events = Rx.single(ServerSentEvent(data = "test"))
197+
val response = Http.response().withContentType("text/event-stream; charset=utf-8").withEvents(events)
198+
199+
// Should preserve the existing content type with charset
200+
response.contentType shouldBe Some("text/event-stream; charset=utf-8")
201+
}
173202
}

airframe-http/src/main/scala/wvlet/airframe/http/HttpHeader.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,6 @@ object HttpHeader {
8787
final val ApplicationJson = "application/json;charset=utf-8"
8888
final val ApplicationMsgPack = "application/msgpack"
8989
final val OctetStream = "application/octet-stream"
90+
final val TextEventStream = "text/event-stream"
9091
}
9192
}

airframe-http/src/main/scala/wvlet/airframe/http/HttpMessage.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,26 @@ object HttpMessage {
332332

333333
def statusCode: Int = status.code
334334
def withStatus(newStatus: HttpStatus): Response = this.copy(status = newStatus)
335+
336+
/**
337+
* Set Server-Sent Events stream for SSE responses. This method also sets the Content-Type header to
338+
* "text/event-stream" if not already set.
339+
*
340+
* @param events
341+
* Rx stream of ServerSentEvent
342+
* @return
343+
* Response with SSE events
344+
*/
345+
def withEvents(newEvents: Rx[ServerSentEvent]): Response = {
346+
if (isContentTypeEventStream) {
347+
this.copy(events = newEvents)
348+
} else {
349+
this.copy(
350+
header = header.set(HttpHeader.ContentType, HttpHeader.MediaType.TextEventStream),
351+
events = newEvents
352+
)
353+
}
354+
}
335355
}
336356

337357
object Response {

0 commit comments

Comments
 (0)