Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Reactive Route stream serialization without relying on the Multi sub-type #21643

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions docs/src/main/asciidoc/reactive-routes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -399,18 +399,16 @@ The previous snippet produces:

You can return a `Multi` to produce a JSON Array, where every item is an item from this array.
The response is written item by item to the client.
The `content-type` is set to `application/json` if not set already.

To use this feature, you need to wrap the returned `Multi` using `io.quarkus.vertx.web.ReactiveRoutes.asJsonArray`:
To do that set the `produces` attribute to `"application/json"` (or `ReactiveRoutes.APPLICATION_JSON`).

[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.APPLICATION_JSON)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonArray(Multi.createFrom().items(
return Multi.createFrom().items(
new Person("superman", 1),
new Person("batman", 2),
new Person("spiderman", 3)));
new Person("spiderman", 3));
}
----

Expand All @@ -425,24 +423,34 @@ The previous snippet produces:
]
----

TIP: The `produces` attribute is an array.
When you pass a single value you can omit the "{" and "}".
Note that `"application/json"` must be the first value in the array.

Only `Multi<String>`, `Multi<Object>` and `Multi<Void>` can be written into the JSON Array.
Using a `Multi<Void>` produces an empty array.
You cannot use `Multi<Buffer>`.
If you need to use `Buffer`, transform the content into a JSON or String representation first.

[NOTE]
.Deprecation of `asJsonArray`
====
The `ReactiveRoutes.asJsonArray` has been deprecated as it is not compatible with the security layer of Quarkus.
====

=== Event Stream and Server-Sent Event support

You can return a `Multi` to produce an event source (stream of server sent events).
To enable this feature, you need to wrap the returned `Multi` using `io.quarkus.vertx.web.ReactiveRoutes.asEventStream`:
To enable this feature, set the `produces` attribute to `"text/event-stream"` (or `ReactiveRoutes.EVENT_STREAM`), such as in:

[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.EVENT_STREAM)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asEventStream(Multi.createFrom().items(
return Multi.createFrom().items(
new Person("superman", 1),
new Person("batman", 2),
new Person("spiderman", 3)));
new Person("spiderman", 3));
}
----

Expand All @@ -461,6 +469,10 @@ id: 2

----

TIP: The `produces` attribute is an array.
When you pass a single value you can omit the "{" and "}".
Note that `"text/event-stream"` must be the first value in the array.

You can also implement the `io.quarkus.vertx.web.ReactiveRoutes.ServerSentEvent` interface to customize the `event` and `id` section of the server sent event:

[source, java]
Expand Down Expand Up @@ -491,7 +503,7 @@ class PersonEvent implements ReactiveRoutes.ServerSentEvent<Person> {
}
----

Using a `Multi<PersonEvent>` (wrapped using `io.quarkus.vertx.web.ReactiveRoutes.asEventStream`) would produce:
Using a `Multi<PersonEvent>` would produce:

[source, text]
----
Expand All @@ -509,14 +521,20 @@ id: 3

----

[NOTE]
.Deprecation of `asEventStream`
====
The `ReactiveRoutes.asEventStream` has been deprecated as it is not compatible with the security layer of Quarkus.
====

=== Json Stream in NDJSON format

You can return a `Multi` to produce a newline delimited stream of JSON values.
To enable this feature, you need to wrap the returned `Multi` using `io.quarkus.vertx.web.ReactiveRoutes.asJsonStream`:
To enable this feature, set the `produces` attribute of the `@Route` annotation to `"application/x-ndjson"` (or `ReactiveRoutes.ND_JSON`):

[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.ND_JSON)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonStream(Multi.createFrom().items(
new Person("superman", 1),
Expand All @@ -536,11 +554,14 @@ This method would produce:

----

TIP: The `produces` attribute is an array. When you pass a single value you can omit the "{" and "}".
Note that `"application/x-ndjson"` must be the first value in the array.

You can also provide strings instead of Objects, in that case the strings will be wrapped in quotes to become valid JSON values:

[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.ND_JSON)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonStream(Multi.createFrom().items(
"superman",
Expand All @@ -558,6 +579,12 @@ Multi<Person> people(RoutingContext context) {

----

[NOTE]
.Deprecation of `asJsonStream`
====
The `ReactiveRoutes.asJsonStream` has been deprecated as it is not compatible with the security layer of Quarkus.
====

=== Using Bean Validation

You can combine reactive routes and Bean Validation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,32 @@ class HandlerDescriptor {
private final MethodInfo method;
private final BeanValidationAnnotationsBuildItem validationAnnotations;
private final HandlerType handlerType;
private final Type contentType;
private final Type payloadType;
private final String[] contentTypes;

HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType) {
HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType,
String[] producedTypes) {
this.method = method;
this.validationAnnotations = bvAnnotations;
this.handlerType = handlerType;
Type returnType = method.returnType();
if (returnType.kind() == Kind.VOID) {
contentType = null;
payloadType = null;
} else {
if (returnType.name().equals(DotNames.UNI) || returnType.name().equals(DotNames.MULTI)
|| returnType.name().equals(DotNames.COMPLETION_STAGE)) {
contentType = returnType.asParameterizedType().arguments().get(0);
payloadType = returnType.asParameterizedType().arguments().get(0);
} else {
contentType = returnType;
payloadType = returnType;
}
}
this.contentTypes = producedTypes;
}

Type getReturnType() {
return method.returnType();
}

boolean isReturningVoid() {
return method.returnType().kind().equals(Type.Kind.VOID);
}

boolean isReturningUni() {
return method.returnType().name().equals(DotNames.UNI);
}
Expand All @@ -55,6 +54,13 @@ boolean isReturningCompletionStage() {
return method.returnType().name().equals(DotNames.COMPLETION_STAGE);
}

public String getFirstContentType() {
if (contentTypes == null || contentTypes.length == 0) {
return null;
}
return contentTypes[0];
}

/**
* @return {@code true} if the method is annotated with a constraint or {@code @Valid} or any parameter has such kind of
* annotation.
Expand Down Expand Up @@ -86,28 +92,28 @@ boolean isProducedResponseValidated() {
return false;
}

Type getContentType() {
return contentType;
Type getPayloadType() {
return payloadType;
}

boolean isContentTypeString() {
Type type = getContentType();
boolean isPayloadString() {
Type type = getPayloadType();
if (type == null) {
return false;
}
return type.name().equals(io.quarkus.arc.processor.DotNames.STRING);
}

boolean isContentTypeBuffer() {
Type type = getContentType();
boolean isPayloadTypeBuffer() {
Type type = getPayloadType();
if (type == null) {
return false;
}
return type.name().equals(DotNames.BUFFER);
}

boolean isContentTypeMutinyBuffer() {
Type type = getContentType();
boolean isPayloadMutinyBuffer() {
Type type = getPayloadType();
if (type == null) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ static void returnAndClose(BytecodeCreator creator) {
}

static boolean isNoContent(HandlerDescriptor descriptor) {
return descriptor.getContentType().name()
return descriptor.getPayloadType().name()
.equals(DotName.createSimple(Void.class.getName()));
}

Expand All @@ -249,7 +249,7 @@ static ResultHandle createNpeBecauseItemIfNull(BytecodeCreator writer) {
}

static MethodDescriptor getEndMethodForContentType(HandlerDescriptor descriptor) {
if (descriptor.isContentTypeBuffer() || descriptor.isContentTypeMutinyBuffer()) {
if (descriptor.isPayloadTypeBuffer() || descriptor.isPayloadMutinyBuffer()) {
return END_WITH_BUFFER;
}
return END_WITH_STRING;
Expand Down
Loading