diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml
index 74e48b0d..84c6b219 100644
--- a/examples/webserver/pom.xml
+++ b/examples/webserver/pom.xml
@@ -53,5 +53,6 @@
tracing
tutorial
websocket
+ sse
diff --git a/examples/webserver/sse/README.md b/examples/webserver/sse/README.md
new file mode 100644
index 00000000..1742e306
--- /dev/null
+++ b/examples/webserver/sse/README.md
@@ -0,0 +1,16 @@
+# Helidon SE SSE Example
+
+This example demonstrates how to use Server Sent Events (SSE) with both the WebServer and WebClient APIs.
+
+This project implements a couple of SSE endpoints that send either text or JSON messages.
+The unit test uses the WebClient API to test the endpoints.
+
+## Build, run and test
+
+Build and start the server:
+```shell
+mvn package
+java -jar target/helidon-examples-webserver-sse.jar
+```
+
+Then open http://localhost:8080 in your browser.
diff --git a/examples/webserver/sse/pom.xml b/examples/webserver/sse/pom.xml
new file mode 100644
index 00000000..a471da0f
--- /dev/null
+++ b/examples/webserver/sse/pom.xml
@@ -0,0 +1,96 @@
+
+
+
+ 4.0.0
+
+ io.helidon.applications
+ helidon-se
+ 4.2.0-SNAPSHOT
+
+
+ io.helidon.examples.webserver
+ helidon-examples-webserver-sse
+ 1.0.0-SNAPSHOT
+ Helidon Examples WebServer SSE
+
+
+ io.helidon.examples.webserver.sse.Main
+
+
+
+
+ io.helidon.webserver
+ helidon-webserver
+
+
+ io.helidon.webserver
+ helidon-webserver-sse
+
+
+ io.helidon.webserver
+ helidon-webserver-static-content
+
+
+ jakarta.json
+ jakarta.json-api
+
+
+ io.helidon.http.media
+ helidon-http-media-jsonp
+
+
+ io.helidon.logging
+ helidon-logging-jul
+
+
+ io.helidon.webclient
+ helidon-webclient-sse
+ test
+
+
+ io.helidon.webserver.testing.junit5
+ helidon-webserver-testing-junit5
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-libs
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java
new file mode 100644
index 00000000..9f90c429
--- /dev/null
+++ b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.examples.webserver.sse;
+
+import io.helidon.logging.common.LogConfig;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.staticcontent.StaticContentService;
+
+/**
+ * This application provides a simple service with a UI to exercise Server Sent Events (SSE).
+ */
+class Main {
+
+ private Main() {
+ }
+
+ /**
+ * Executes the example.
+ *
+ * @param args command line arguments, ignored
+ */
+ public static void main(String[] args) {
+ // load logging configuration
+ LogConfig.configureRuntime();
+
+ WebServer server = WebServer.builder()
+ .routing(Main::routing)
+ .port(8080)
+ .build()
+ .start();
+
+ System.out.println("WEB server is up! http://localhost:" + server.port());
+ }
+
+ /**
+ * Updates the routing rules.
+ *
+ * @param rules routing rules
+ */
+ static void routing(HttpRules rules) {
+ rules.register("/", StaticContentService.builder("WEB")
+ .welcomeFileName("index.html")
+ .build())
+ .register("/api", new SseService());
+ }
+}
diff --git a/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java
new file mode 100644
index 00000000..eb21de21
--- /dev/null
+++ b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.examples.webserver.sse;
+
+import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
+import java.time.Duration;
+
+import io.helidon.http.sse.SseEvent;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+import io.helidon.webserver.sse.SseSink;
+
+import jakarta.json.spi.JsonProvider;
+
+/**
+ * SSE service.
+ */
+class SseService implements HttpService {
+
+ private static final System.Logger LOGGER = System.getLogger(SseService.class.getName());
+ private static final JsonProvider JSON_PROVIDER = JsonProvider.provider();
+
+ @Override
+ public void routing(HttpRules httpRules) {
+ httpRules.get("/sse_text", this::sseText)
+ .get("/sse_json", this::sseJson);
+ }
+
+ void sseText(ServerRequest req, ServerResponse res) throws InterruptedException {
+ int count = req.query().first("count").asInt().orElse(1);
+ int delay = req.query().first("delay").asInt().orElse(0);
+ try (SseSink sseSink = res.sink(SseSink.TYPE)) {
+ for (int i = 0; i < count; i++) {
+ try {
+ sseSink.emit(SseEvent.builder()
+ .comment("comment#" + i)
+ .name("my-event")
+ .data("data#" + i)
+ .build());
+ } catch (UncheckedIOException e) {
+ LOGGER.log(Level.DEBUG, e.getMessage()); // connection close?
+ return;
+ }
+
+ if (delay > 0) {
+ Thread.sleep(Duration.ofSeconds(delay));
+ }
+ }
+ }
+ }
+
+ void sseJson(ServerRequest req, ServerResponse res) throws InterruptedException {
+ int count = req.query().first("count").asInt().orElse(1);
+ int delay = req.query().first("delay").asInt().orElse(0);
+ try (SseSink sseSink = res.sink(SseSink.TYPE)) {
+ for (int i = 0; i < count; i++) {
+ try {
+ sseSink.emit(SseEvent.create(JSON_PROVIDER.createObjectBuilder()
+ .add("data", "data#" + i)
+ .build()));
+ } catch (UncheckedIOException e) {
+ LOGGER.log(Level.DEBUG, e.getMessage()); // connection close?
+ return;
+ }
+
+ if (delay > 0) {
+ Thread.sleep(Duration.ofSeconds(delay));
+ }
+ }
+ }
+ }
+}
diff --git a/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java
new file mode 100644
index 00000000..687abe6d
--- /dev/null
+++ b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Helidon Examples WebServer SSE.
+ */
+package io.helidon.examples.webserver.sse;
diff --git a/examples/webserver/sse/src/main/resources/WEB/index.html b/examples/webserver/sse/src/main/resources/WEB/index.html
new file mode 100644
index 00000000..633518a8
--- /dev/null
+++ b/examples/webserver/sse/src/main/resources/WEB/index.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+ Helidon Examples WebServer SSE
+
+
+
+
+Text events
+
+JSON events
+
+
+
+
diff --git a/examples/webserver/sse/src/main/resources/logging.properties b/examples/webserver/sse/src/main/resources/logging.properties
new file mode 100644
index 00000000..452ea9d0
--- /dev/null
+++ b/examples/webserver/sse/src/main/resources/logging.properties
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2024 Oracle and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+handlers=io.helidon.logging.jul.HelidonConsoleHandler
+java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n
+# Global logging level. Can be overridden by specific loggers
+.level=INFO
diff --git a/examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java b/examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java
new file mode 100644
index 00000000..e249ccb0
--- /dev/null
+++ b/examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.examples.webserver.sse;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.json.JsonObject;
+
+import io.helidon.http.sse.SseEvent;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.sse.SseSource;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpRoute;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.http.HeaderValues.ACCEPT_EVENT_STREAM;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@ServerTest
+class SseServiceTest {
+
+ private final Http1Client client;
+
+ SseServiceTest(Http1Client client) {
+ this.client = client;
+ }
+
+ @SetUpRoute
+ static void routing(HttpRules rules) {
+ rules.register(new SseService());
+ }
+
+ @Test
+ void testSseText() throws InterruptedException {
+ try (var response = client.get("/sse_text")
+ .queryParam("count", "3")
+ .header(ACCEPT_EVENT_STREAM).request()) {
+ var latch = new CountDownLatch(3);
+ var events = new ArrayList();
+ response.source(SseSource.TYPE, event -> {
+ events.add(event);
+ latch.countDown();
+ });
+ assertThat(latch.await(5, TimeUnit.SECONDS), is(true));
+ assertThat(events.size(), is(3));
+ for (int i = 0; i < 3; i++) {
+ var event = events.get(i);
+ assertThat(event.comment().orElse(null), is("comment#" + i));
+ assertThat(event.name().orElse(null), is("my-event"));
+ assertThat(event.data(String.class), is("data#" + i));
+ }
+ }
+ }
+
+ @Test
+ void testSseJson() throws InterruptedException {
+ try (var response = client.get("/sse_json")
+ .queryParam("count", "3")
+ .header(ACCEPT_EVENT_STREAM).request()) {
+ var latch = new CountDownLatch(3);
+ var events = new ArrayList();
+ response.source(SseSource.TYPE, event -> {
+ events.add(event.data(JsonObject.class));
+ latch.countDown();
+ });
+ assertThat(latch.await(5, TimeUnit.SECONDS), is(true));
+ assertThat(events.size(), is(3));
+ for (int i = 0; i < 3; i++) {
+ var event = events.get(i);
+ assertThat(event, is(notNullValue()));
+ assertThat(event.getString("data", null), is("data#" + i));
+ }
+ }
+ }
+}