Skip to content
Streamlord

Spring WebMVC

datastarStream on a servlet response, and the one bean that configures it.

Spring is the Shield. Spring and the servlet API are compileOnly, so the adapter adds no version of either. It compiles against Framework 6.2 and is tested against 6.2 (Boot 3) and 7.0 (Boot 4) on every build.

One bean, declared by you

There is no auto-configuration, on purpose. An adapter that configures itself is an adapter you have to un-configure the day it guesses wrong.

@Bean
fun streamlord(mapper: tools.jackson.databind.ObjectMapper): Streamlord =
    Streamlord(codec = JacksonSignalsCodec(mapper))

Pick the codec that matches your Boot generation: JacksonSignalsCodec from streamlord-json-jackson for Boot 4, Jackson2SignalsCodec from streamlord-json-jackson2 for Boot 3. The helpers take the bean explicitly, which is what keeps the adapter free of a component scan.

Streaming from a controller

@PostMapping("/search")
fun search(
    request: HttpServletRequest,
    response: HttpServletResponse,
    streamlord: Streamlord,
): org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody {
    val signals = request.readSignalsOr(SearchSignals(), streamlord)
    val hits = repository.search(signals.query)

    return response.datastarStream(streamlord) {
        patchElements(
            renderResults(hits),
            selector = "#results",
            mode = ElementPatchMode.INNER,
        )
        patchSignals("total" to hits.size)
    }
}

datastarStream returns a StreamingResponseBody, which is how WebMVC keeps a servlet thread from being held for the life of the response. Everything inside the block writes one frame.

No stream at all

@GetMapping("/panel")
fun panel(): org.springframework.http.ResponseEntity<String> =
    datastarElements("""<div id="panel">Quiet.</div>""")

A plain text/html response, which Datastar reads as an element patch. When you want the guard on that HTML, pass the bean: datastarElements(html, streamlord = bean).

Reading signals

@GetMapping("/page")
fun page(request: HttpServletRequest, streamlord: Streamlord): String {
    val signals = request.readSignals(streamlord)
    val query = signals.string("search") ?: ""
    return query
}

The same protocol rule applies as everywhere: query parameter for GET and DELETE, body for the rest.

What this page does not cover

SignalsTooLargeException should be mapped to 413 in your @ControllerAdvice; Streamlord does not register one, because registering exception handlers behind your back is exactly the kind of help that turns into a fight. For reactive endpoints, see Spring WebFlux. The one filter the adapter offers, CspNonceFilter, is a bean you declare like any other, and it is on Security.

What went over the wire

The frames your last search produced, encoded by the same SseEncoder the golden-file tests check. Not a description of them. The frames.

Nothing yet. Search from the top of the page, and what the server sends will appear here.