Skip to content

Commit

Permalink
Introduce a way to set headers and status code for streaming response
Browse files Browse the repository at this point in the history
Relates to: quarkusio#33130
  • Loading branch information
geoand authored and manofthepeace committed May 16, 2023
1 parent 418f41a commit f4cc168
Show file tree
Hide file tree
Showing 16 changed files with 245 additions and 13 deletions.
32 changes: 32 additions & 0 deletions docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,38 @@ impression that you can set headers or HTTP status codes, which is not true afte
response.
Exception mappers are also not invoked because part of the response may already have been written.

[TIP]
====
If you need to set custom HTTP headers and / or the HTTP response, then you can return `org.jboss.resteasy.reactive.RestMulti` instead, like this:
[source,java]
----
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.reactive.messaging.Channel;
import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;
@Path("logs")
public class Endpoint {
@Inject
@Channel("log-out")
Multi<String> logs;
@GET
public Multi<String> streamLogs() {
return RestMulti.from(logs).status(222).header("foo", "bar").build();
}
}
----
====

=== Server-Sent Event (SSE) support

If you want to stream JSON objects in your response, you can use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PART_TYPE_NAME;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.UNI;

import java.io.Closeable;
Expand Down Expand Up @@ -204,7 +205,7 @@ public class JaxrsClientReactiveProcessor {

private static final DotName NOT_BODY = DotName.createSimple("io.quarkus.rest.client.reactive.NotBody");

private static final Set<DotName> ASYNC_RETURN_TYPES = Set.of(COMPLETION_STAGE, UNI, MULTI);
private static final Set<DotName> ASYNC_RETURN_TYPES = Set.of(COMPLETION_STAGE, UNI, MULTI, REST_MULTI);
public static final DotName BYTE = DotName.createSimple(Byte.class.getName());
public static final MethodDescriptor MULTIPART_RESPONSE_DATA_ADD_FILLER = MethodDescriptor
.ofMethod(MultipartResponseDataBase.class, "addFiller", void.class, FieldFiller.class);
Expand Down Expand Up @@ -1937,7 +1938,7 @@ private void handleReturn(ClassInfo restClientInterface, String defaultMediaType
if (ASYNC_RETURN_TYPES.contains(paramType.name())) {
returnCategory = paramType.name().equals(COMPLETION_STAGE)
? ReturnCategory.COMPLETION_STAGE
: paramType.name().equals(MULTI)
: paramType.name().equals(MULTI) || paramType.name().equals(REST_MULTI)
? ReturnCategory.MULTI
: ReturnCategory.UNI;

Expand Down Expand Up @@ -2738,6 +2739,7 @@ private enum ReturnCategory {
COMPLETION_STAGE,
UNI,
MULTI,
REST_MULTI,
COROUTINE
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r
effectiveReturnType.name().equals(ResteasyReactiveDotNames.UNI) ||
effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETABLE_FUTURE) ||
effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETION_STAGE) ||
effectiveReturnType.name().equals(ResteasyReactiveDotNames.REST_MULTI) ||
effectiveReturnType.name().equals(ResteasyReactiveDotNames.MULTI)) {
if (effectiveReturnType.kind() != Type.Kind.PARAMETERIZED_TYPE) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.ws.rs.sse.SseBroadcaster;
import jakarta.ws.rs.sse.SseEventSink;

import org.jboss.resteasy.reactive.RestMulti;
import org.jboss.resteasy.reactive.RestStreamElementType;
import org.jboss.resteasy.reactive.common.util.RestMediaType;

Expand Down Expand Up @@ -97,7 +98,8 @@ public void sseJson2(Sse sse, SseEventSink sink) throws IOException {
@GET
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<Message> multiJson() {
return Multi.createFrom().items(new Message("hello"), new Message("stef"));
return RestMulti.from(Multi.createFrom().items(new Message("hello"), new Message("stef")))
.header("foo", "bar").build();
}

@Path("json/multi2")
Expand Down Expand Up @@ -135,9 +137,9 @@ public Multi<Message> multiStreamJsonFast() {
for (int i = 0; i < 5000; i++) {
ids.add(UUID.randomUUID());
}
return Multi.createFrom().items(ids::stream)
return RestMulti.from(Multi.createFrom().items(ids::stream)
.onItem().transform(id -> new Message(id.toString()))
.onOverflow().buffer(81920);
.onOverflow().buffer(81920)).header("foo", "bar").build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ private ClassInfo getEffectiveClassInfo(Type type, IndexView indexView) {
effectiveType.name().equals(ResteasyReactiveDotNames.UNI) ||
effectiveType.name().equals(ResteasyReactiveDotNames.COMPLETABLE_FUTURE) ||
effectiveType.name().equals(ResteasyReactiveDotNames.COMPLETION_STAGE) ||
effectiveType.name().equals(ResteasyReactiveDotNames.REST_MULTI) ||
effectiveType.name().equals(ResteasyReactiveDotNames.MULTI)) {
if (effectiveType.kind() != Type.Kind.PARAMETERIZED_TYPE) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETABLE_FUTURE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_RESPONSE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.UNI;

Expand Down Expand Up @@ -161,6 +162,7 @@ private Type getNonAsyncReturnType(Type returnType) {
|| COMPLETABLE_FUTURE.equals(parameterizedType.name())
|| UNI.equals(parameterizedType.name())
|| MULTI.equals(parameterizedType.name())
|| REST_MULTI.equals(parameterizedType.name())
|| REST_RESPONSE.equals(parameterizedType.name())) {
return parameterizedType.arguments().get(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DATE_FORMAT;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PUBLISHER;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MULTI;

import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -553,6 +554,7 @@ public void accept(EndpointIndexer.ResourceMethodCallbackEntry entry) {

if (paramsRequireReflection ||
MULTI.toString().equals(entry.getResourceMethod().getSimpleReturnType()) ||
REST_MULTI.toString().equals(entry.getResourceMethod().getSimpleReturnType()) ||
PUBLISHER.toString().equals(entry.getResourceMethod().getSimpleReturnType()) ||
filtersAccessResourceMethod(resourceInterceptorsBuildItem.getResourceInterceptors()) ||
entry.additionalRegisterClassForReflectionCheck()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.ResponseHeader;
import org.jboss.resteasy.reactive.ResponseStatus;
import org.jboss.resteasy.reactive.RestMulti;
import org.jboss.resteasy.reactive.RestQuery;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

Expand Down Expand Up @@ -118,6 +121,40 @@ public void testStringThrowsException() {
assertFalse(headers.hasHeaderWithName("Access-Control-Allow-Origin"));
}

@Test
public void testReturnRestMulti() {
Map<String, String> expectedHeaders = Map.of(
"Access-Control-Allow-Origin", "foo",
"Keep-Alive", "bar");
RestAssured
.given()
.get("/test/rest-multi")
.then()
.statusCode(200)
.headers(expectedHeaders);
}

@Test
public void testReturnRestMulti2() {
RestAssured
.given()
.get("/test/rest-multi2")
.then()
.statusCode(200)
.headers(Map.of(
"Access-Control-Allow-Origin", "foo",
"Keep-Alive", "bar"));

RestAssured
.given()
.get("/test/rest-multi2?keepAlive=dummy")
.then()
.statusCode(200)
.headers(Map.of(
"Access-Control-Allow-Origin", "foo",
"Keep-Alive", "dummy"));
}

@Path("/test")
public static class TestResource {

Expand Down Expand Up @@ -190,6 +227,22 @@ public String throwExceptionPlain() {
throw createException();
}

@ResponseHeader(name = "Access-Control-Allow-Origin", value = "*")
@ResponseHeader(name = "Keep-Alive", value = "timeout=5, max=997")
@GET
@Path("/rest-multi")
public RestMulti<String> getTestRestMulti() {
return RestMulti.from(Multi.createFrom().item("test")).header("Access-Control-Allow-Origin", "foo")
.header("Keep-Alive", "bar").build();
}

@GET
@Path("/rest-multi2")
public RestMulti<String> getTestRestMulti2(@DefaultValue("bar") @RestQuery String keepAlive) {
return RestMulti.from(Multi.createFrom().item("test")).header("Access-Control-Allow-Origin", "foo")
.header("Keep-Alive", keepAlive).build();
}

private IllegalArgumentException createException() {
IllegalArgumentException result = new IllegalArgumentException();
result.setStackTrace(EMPTY_STACK_TRACE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.ResponseStatus;
import org.jboss.resteasy.reactive.RestMulti;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

Expand Down Expand Up @@ -93,6 +94,24 @@ public void testStringThrowsException() {
.statusCode(500);
}

@Test
public void testReturnRestMulti() {
RestAssured
.given()
.get("/test/rest-multi")
.then()
.statusCode(210);
}

@Test
public void testReturnRestMulti2() {
RestAssured
.given()
.get("/test/rest-multi2")
.then()
.statusCode(211);
}

@Path("/test")
public static class TestResource {

Expand Down Expand Up @@ -154,6 +173,19 @@ public String throwExceptionPlain() {
throw createException();
}

@ResponseStatus(202)
@GET
@Path("/rest-multi")
public RestMulti<String> getTestRestMulti() {
return RestMulti.from(Multi.createFrom().item("test")).status(210).build();
}

@GET
@Path("/rest-multi2")
public RestMulti<String> getTestRestMulti2() {
return RestMulti.from(Multi.createFrom().item("test")).status(211).build();
}

private IllegalArgumentException createException() {
IllegalArgumentException result = new IllegalArgumentException();
result.setStackTrace(EMPTY_STACK_TRACE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_HEADER_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MATRIX_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_PATH_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_QUERY_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_RESPONSE;
Expand Down Expand Up @@ -990,6 +991,7 @@ private Type getNonAsyncReturnType(Type returnType) {
|| COMPLETABLE_FUTURE.equals(parameterizedType.name())
|| UNI.equals(parameterizedType.name())
|| MULTI.equals(parameterizedType.name())
|| REST_MULTI.equals(parameterizedType.name())
|| REST_RESPONSE.equals(parameterizedType.name())) {
return parameterizedType.arguments().get(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestMatrix;
import org.jboss.resteasy.reactive.RestMulti;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
import org.jboss.resteasy.reactive.RestResponse;
Expand Down Expand Up @@ -202,6 +203,7 @@ public final class ResteasyReactiveDotNames {

public static final DotName UNI = DotName.createSimple(Uni.class.getName());
public static final DotName MULTI = DotName.createSimple(Multi.class.getName());
public static final DotName REST_MULTI = DotName.createSimple(RestMulti.class.getName());
public static final DotName COMPLETION_STAGE = DotName.createSimple(CompletionStage.class.getName());
public static final DotName COMPLETABLE_FUTURE = DotName.createSimple(CompletableFuture.class.getName());
public static final DotName PUBLISHER = DotName.createSimple(Publisher.class.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.jboss.resteasy.reactive;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap;
import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import io.smallrye.mutiny.operators.AbstractMulti;
import io.smallrye.mutiny.subscription.MultiSubscriber;

/**
* A wrapper around {@link Multi} that gives resource methods a way to specify the HTTP status code and HTTP headers
* when streaming a result.
*/
public class RestMulti<T> extends AbstractMulti<T> {

private final Multi<T> multi;
private final Integer status;
private final MultivaluedTreeMap<String, String> headers;

public static <T> RestMulti.Builder<T> from(Multi<T> multi) {
return new RestMulti.Builder<>(multi);
}

private RestMulti(Builder<T> builder) {
this.multi = builder.multi;
this.status = builder.status;
this.headers = builder.headers;
}

@Override
public void subscribe(MultiSubscriber<? super T> subscriber) {
multi.subscribe(Infrastructure.onMultiSubscription(multi, subscriber));
}

public Integer getStatus() {
return status;
}

public Map<String, List<String>> getHeaders() {
return headers;
}

public static class Builder<T> {
private final Multi<T> multi;

private Integer status;

private final MultivaluedTreeMap<String, String> headers = new CaseInsensitiveMap<>();

private Builder(Multi<T> multi) {
this.multi = Objects.requireNonNull(multi, "multi cannot be null");
}

public Builder<T> status(int status) {
this.status = status;
return this;
}

public Builder<T> header(String name, String value) {
if (value == null) {
headers.remove(name);
return this;
}
headers.add(name, value);
return this;
}

public RestMulti<T> build() {
return new RestMulti<>(this);
}
}
}
Loading

0 comments on commit f4cc168

Please sign in to comment.