6.20 Writing Response Data
Reactively Writing Response Data
Micronaut’s HTTP server supports writing chunks of response data by returning a Publisher that emits objects that can be encoded to the HTTP response.
The following table summarizes example return type signatures and the behaviour the server exhibits to handle them:
Return Type | Description |
---|---|
| A Publisher that emits each chunk of content as a String |
| A reactor:Flux[] that emits each chunk of content as a |
| A Reactor |
| When emitting a POJO, each emitted object is encoded as JSON by default without blocking |
| A reactor:Flux[] that emits each chunk of content as a |
| A Reactor |
| When emitting a POJO, each emitted object is encoded as JSON by default without blocking |
When returning a reactive type, the server uses a Transfer-Encoding
of chunked
and keeps writing data until the Publisher onComplete
method is called.
The server requests a single item from the Publisher, writes it, and requests the next, controlling back pressure.
It is up to the implementation of the Publisher to schedule any blocking I/O work that may be done as a result of subscribing to the publisher. |
To use Project Reactor‘s Flux or Mono you need to add the Micronaut Reactor dependency to your project to include the necessary converters. |
To use RxJava‘s Flowable , Single or Maybe you need to add the Micronaut RxJava dependency to your project to include the necessary converters. |
Performing Blocking I/O
In some cases you may wish to integrate a library that does not support non-blocking I/O.
Writable
In this case you can return a Writable object from any controller method. The Writable interface has various signatures that allow writing to traditional blocking streams like Writer or OutputStream.
When returning a Writable, the blocking I/O operation is shifted to the I/O thread pool so the Netty event loop is not blocked.
See the section on configuring Server Thread Pools for details on how to configure the I/O thread pool to meet your application requirements. |
The following example demonstrates how to use this API with Groovy’s SimpleTemplateEngine
to write a server side template:
Performing Blocking I/O With Writable
import groovy.text.SimpleTemplateEngine;
import groovy.text.Template;
import io.micronaut.core.io.Writable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.server.exceptions.HttpServerException;
@Controller("/template")
public class TemplateController {
private final SimpleTemplateEngine templateEngine = new SimpleTemplateEngine();
private final Template template = initTemplate(); (1)
@Get(value = "/welcome", produces = MediaType.TEXT_PLAIN)
Writable render() { (2)
return writer -> template.make( (3)
CollectionUtils.mapOf(
"firstName", "Fred",
"lastName", "Flintstone"
)
).writeTo(writer);
}
private Template initTemplate() {
try {
return templateEngine.createTemplate(
"Dear $firstName $lastName. Nice to meet you."
);
} catch (Exception e) {
throw new HttpServerException("Cannot create template");
}
}
}
Performing Blocking I/O With Writable
import groovy.text.SimpleTemplateEngine
import groovy.text.Template
import io.micronaut.core.io.Writable
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.server.exceptions.HttpServerException
@Controller("/template")
class TemplateController {
private final SimpleTemplateEngine templateEngine = new SimpleTemplateEngine()
private final Template template = initTemplate() (1)
@Get(value = "/welcome", produces = MediaType.TEXT_PLAIN)
Writable render() { (2)
{ writer ->
template.make( (3)
firstName: "Fred",
lastName: "Flintstone"
).writeTo(writer)
}
}
private Template initTemplate() {
try {
return templateEngine.createTemplate(
'Dear $firstName $lastName. Nice to meet you.'
)
} catch (Exception e) {
throw new HttpServerException("Cannot create template")
}
}
}
Performing Blocking I/O With Writable
import groovy.text.SimpleTemplateEngine
import groovy.text.Template
import io.micronaut.core.io.Writable
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.server.exceptions.HttpServerException
import java.io.Writer
@Controller("/template")
class TemplateController {
private val templateEngine = SimpleTemplateEngine()
private val template = initTemplate() (1)
@Get(value = "/welcome", produces = [MediaType.TEXT_PLAIN])
internal fun render(): Writable { (2)
return { writer: Writer ->
template.make( (3)
mapOf(
"firstName" to "Fred",
"lastName" to "Flintstone"
)
).writeTo(writer)
} as Writable
}
private fun initTemplate(): Template {
return try {
templateEngine.createTemplate(
"Dear \$firstName \$lastName. Nice to meet you."
)
} catch (e: Exception) {
throw HttpServerException("Cannot create template")
}
}
}
1 | The controller creates a simple template |
2 | The controller method returns a Writable |
3 | The returned function receives a Writer and calls writeTo on the template. |
InputStream
Another option is to return an input stream. This is useful for many scenarios that interact with other APIs that expose a stream.
Performing Blocking I/O With InputStream
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
return new ByteArrayInputStream(bytes); (1)
}
Performing Blocking I/O With InputStream
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
new ByteArrayInputStream(bytes) (1)
}
Performing Blocking I/O With InputStream
@Get(value = "/write", produces = [MediaType.TEXT_PLAIN])
fun write(): InputStream {
val bytes = "test".toByteArray(StandardCharsets.UTF_8)
return ByteArrayInputStream(bytes) (1)
}
1 | The input stream is returned and its contents will be the response body |
The reading of the stream will be offloaded to the IO thread pool if the controller method is executed on the event loop. |
404 Responses
Often, you want to respond 404 (Not Found) when you don’t find an item in your persistence layer or in similar scenarios.
See the following example:
@Controller("/books")
public class BooksController {
@Get("/stock/{isbn}")
public Map stock(String isbn) {
return null; (1)
}
@Get("/maybestock/{isbn}")
@SingleResult
public Publisher<Map> maybestock(String isbn) {
return Mono.empty(); (2)
}
}
@Controller("/books")
class BooksController {
@Get("/stock/{isbn}")
Map stock(String isbn) {
null (1)
}
@Get("/maybestock/{isbn}")
Mono<Map> maybestock(String isbn) {
Mono.empty() (2)
}
}
@Controller("/books")
class BooksController {
@Get("/stock/{isbn}")
fun stock(isbn: String): Map<*, *>? {
return null (1)
}
@Get("/maybestock/{isbn}")
fun maybestock(isbn: String): Mono<Map<*, *>> {
return Mono.empty() (2)
}
}
1 | Returning null triggers a 404 (Not Found) response. |
2 | Returning an empty Mono triggers a 404 (Not Found) response. |
Responding with an empty Publisher or Flux results in an empty array being returned if the content type is JSON. |