Skip to content

Commit 0006957

Browse files
committed
Support ResponseStatus on reactive controllers
This commit adds support for `@ResponseStatus` annotations on reactive controller methods. `HandlerResultHandler`s implementations now set the status on the `ServerWebExchange`, if and only if the invocation of the controller method succeeded. Issue: SPR-14830
1 parent 87e0151 commit 0006957

File tree

6 files changed

+65
-13
lines changed

6 files changed

+65
-13
lines changed
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
import java.util.Optional;
2424
import java.util.Set;
2525

26+
import org.springframework.core.MethodParameter;
2627
import org.springframework.core.Ordered;
2728
import org.springframework.core.ReactiveAdapterRegistry;
29+
import org.springframework.core.annotation.AnnotationUtils;
2830
import org.springframework.http.MediaType;
2931
import org.springframework.util.Assert;
32+
import org.springframework.web.bind.annotation.ResponseStatus;
3033
import org.springframework.web.reactive.HandlerMapping;
3134
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
3235
import org.springframework.web.server.ServerWebExchange;
@@ -38,7 +41,7 @@
3841
* @author Rossen Stoyanchev
3942
* @since 5.0
4043
*/
41-
public abstract class ContentNegotiatingResultHandlerSupport implements Ordered {
44+
public abstract class AbstractHandlerResultHandler implements Ordered {
4245

4346
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
4447

@@ -50,11 +53,11 @@ public abstract class ContentNegotiatingResultHandlerSupport implements Ordered
5053
private int order = LOWEST_PRECEDENCE;
5154

5255

53-
protected ContentNegotiatingResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver) {
56+
protected AbstractHandlerResultHandler(RequestedContentTypeResolver contentTypeResolver) {
5457
this(contentTypeResolver, new ReactiveAdapterRegistry());
5558
}
5659

57-
protected ContentNegotiatingResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver,
60+
protected AbstractHandlerResultHandler(RequestedContentTypeResolver contentTypeResolver,
5861
ReactiveAdapterRegistry adapterRegistry) {
5962

6063
Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required.");
@@ -151,4 +154,17 @@ private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType pr
151154
return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
152155
}
153156

157+
/**
158+
* Optionally set the response status using the information provided by {@code @ResponseStatus}.
159+
* @param methodParameter the controller method return parameter
160+
* @param exchange the server exchange being handled
161+
*/
162+
protected void updateResponseStatus(MethodParameter methodParameter, ServerWebExchange exchange) {
163+
ResponseStatus annotation = methodParameter.getMethodAnnotation(ResponseStatus.class);
164+
if (annotation != null) {
165+
annotation = AnnotationUtils.synthesizeAnnotation(annotation, methodParameter.getMethod());
166+
exchange.getResponse().setStatusCode(annotation.code());
167+
}
168+
}
169+
154170
}

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import org.springframework.http.server.reactive.ServerHttpResponse;
3434
import org.springframework.util.Assert;
3535
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
36-
import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport;
36+
import org.springframework.web.reactive.result.AbstractHandlerResultHandler;
3737
import org.springframework.web.server.NotAcceptableStatusException;
3838
import org.springframework.web.server.ServerWebExchange;
3939

@@ -44,7 +44,7 @@
4444
* @author Rossen Stoyanchev
4545
* @since 5.0
4646
*/
47-
public abstract class AbstractMessageWriterResultHandler extends ContentNegotiatingResultHandlerSupport {
47+
public abstract class AbstractMessageWriterResultHandler extends AbstractHandlerResultHandler {
4848

4949
private final List<HttpMessageWriter<?>> messageWriters;
5050

@@ -110,7 +110,8 @@ protected Mono<Void> writeBody(Object body, MethodParameter bodyParameter, Serve
110110
}
111111

112112
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
113-
return Mono.from((Publisher<Void>) publisher);
113+
return Mono.from((Publisher<Void>) publisher)
114+
.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
114115
}
115116

116117
List<MediaType> producibleTypes = getProducibleMediaTypes(elementType);
@@ -125,11 +126,12 @@ protected Mono<Void> writeBody(Object body, MethodParameter bodyParameter, Serve
125126
if (bestMediaType != null) {
126127
for (HttpMessageWriter<?> messageWriter : getMessageWriters()) {
127128
if (messageWriter.canWrite(elementType, bestMediaType)) {
128-
return (messageWriter instanceof ServerHttpMessageWriter ?
129-
((ServerHttpMessageWriter<?>)messageWriter).write((Publisher) publisher,
129+
Mono<Void> bodyWriter = (messageWriter instanceof ServerHttpMessageWriter ?
130+
((ServerHttpMessageWriter<?>) messageWriter).write((Publisher) publisher,
130131
bodyType, elementType, bestMediaType, request, response, Collections.emptyMap()) :
131132
messageWriter.write((Publisher) publisher, elementType,
132133
bestMediaType, response, Collections.emptyMap()));
134+
return bodyWriter.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
133135
}
134136
}
135137
}

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import org.springframework.web.reactive.HandlerResult;
4444
import org.springframework.web.reactive.HandlerResultHandler;
4545
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
46-
import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport;
46+
import org.springframework.web.reactive.result.AbstractHandlerResultHandler;
4747
import org.springframework.web.server.NotAcceptableStatusException;
4848
import org.springframework.web.server.ServerWebExchange;
4949
import org.springframework.web.util.HttpRequestPathHelper;
@@ -74,7 +74,7 @@
7474
* @author Rossen Stoyanchev
7575
* @since 5.0
7676
*/
77-
public class ViewResolutionResultHandler extends ContentNegotiatingResultHandlerSupport
77+
public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
7878
implements HandlerResultHandler, Ordered {
7979

8080
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
@@ -208,6 +208,7 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
208208
}
209209
Map<String, ?> model = result.getModel();
210210
return viewMono.then(view -> {
211+
updateResponseStatus(result.getReturnTypeSource(), exchange);
211212
if (view instanceof View) {
212213
return ((View) view).render(model, null, exchange);
213214
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@
4646
import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
4747

4848
/**
49-
* Unit tests for {@link ContentNegotiatingResultHandlerSupport}.
49+
* Unit tests for {@link AbstractHandlerResultHandler}.
5050
* @author Rossen Stoyanchev
5151
*/
52-
public class ContentNegotiatingResultHandlerSupportTests {
52+
public class HandlerResultHandlerTests {
5353

5454
private TestResultHandler resultHandler;
5555

@@ -117,7 +117,7 @@ public void noConcreteMediaType() throws Exception {
117117

118118

119119
@SuppressWarnings("WeakerAccess")
120-
private static class TestResultHandler extends ContentNegotiatingResultHandlerSupport {
120+
private static class TestResultHandler extends AbstractHandlerResultHandler {
121121

122122
protected TestResultHandler() {
123123
this(new HeaderContentTypeResolver());

spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
import org.junit.Before;
2424
import org.junit.Test;
2525
import reactor.core.publisher.Mono;
26+
import reactor.test.StepVerifier;
2627
import rx.Completable;
2728
import rx.Single;
2829

2930
import org.springframework.core.codec.ByteBufferEncoder;
3031
import org.springframework.core.codec.CharSequenceEncoder;
3132
import org.springframework.http.HttpMethod;
33+
import org.springframework.http.HttpStatus;
3234
import org.springframework.http.ResponseEntity;
3335
import org.springframework.http.codec.EncoderHttpMessageWriter;
3436
import org.springframework.http.codec.HttpMessageWriter;
@@ -42,6 +44,7 @@
4244
import org.springframework.ui.ExtendedModelMap;
4345
import org.springframework.util.ObjectUtils;
4446
import org.springframework.web.bind.annotation.ResponseBody;
47+
import org.springframework.web.bind.annotation.ResponseStatus;
4548
import org.springframework.web.bind.annotation.RestController;
4649
import org.springframework.web.method.HandlerMethod;
4750
import org.springframework.web.reactive.HandlerResult;
@@ -114,6 +117,22 @@ public void supports() throws NoSuchMethodException {
114117
testSupports(controller, "handleToMonoResponseEntity", false);
115118
}
116119

120+
@Test
121+
public void writeResponseStatus() throws NoSuchMethodException {
122+
Object controller = new TestRestController();
123+
HandlerMethod hm = handlerMethod(controller, "handleToString");
124+
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
125+
126+
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
127+
assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode());
128+
129+
hm = handlerMethod(controller, "handleToMonoVoid");
130+
handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
131+
132+
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
133+
assertEquals(HttpStatus.CREATED, this.response.getStatusCode());
134+
}
135+
117136
private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException {
118137
HandlerMethod hm = handlerMethod(controller, method);
119138
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
@@ -134,6 +153,10 @@ private HandlerMethod handlerMethod(Object controller, String method) throws NoS
134153
@RestController @SuppressWarnings("unused")
135154
private static class TestRestController {
136155

156+
@ResponseStatus(code = HttpStatus.CREATED)
157+
public Mono<Void> handleToMonoVoid() { return null;}
158+
159+
@ResponseStatus(code = HttpStatus.NO_CONTENT)
137160
public String handleToString() {
138161
return null;
139162
}

spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
4343
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
4444
import org.springframework.http.HttpMethod;
45+
import org.springframework.http.HttpStatus;
4546
import org.springframework.http.MediaType;
4647
import org.springframework.http.server.reactive.ServerHttpResponse;
4748
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
@@ -50,6 +51,7 @@
5051
import org.springframework.ui.Model;
5152
import org.springframework.ui.ModelMap;
5253
import org.springframework.web.bind.annotation.ModelAttribute;
54+
import org.springframework.web.bind.annotation.ResponseStatus;
5355
import org.springframework.web.reactive.HandlerResult;
5456
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
5557
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@@ -128,18 +130,22 @@ public void handleReturnValueTypes() throws Exception {
128130
returnType = ResolvableType.forClass(View.class);
129131
returnValue = new TestView("account");
130132
testHandle("/path", returnType, returnValue, "account: {id=123}");
133+
assertEquals(HttpStatus.NO_CONTENT, this.exchange.getResponse().getStatusCode());
131134

132135
returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class);
133136
returnValue = Mono.just(new TestView("account"));
134137
testHandle("/path", returnType, returnValue, "account: {id=123}");
138+
assertEquals(HttpStatus.SEE_OTHER, this.exchange.getResponse().getStatusCode());
135139

136140
returnType = ResolvableType.forClass(String.class);
137141
returnValue = "account";
138142
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
143+
assertEquals(HttpStatus.CREATED, this.exchange.getResponse().getStatusCode());
139144

140145
returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class);
141146
returnValue = Mono.just("account");
142147
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
148+
assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode());
143149

144150
returnType = ResolvableType.forClass(Model.class);
145151
returnValue = new ExtendedModelMap().addAttribute("name", "Joe");
@@ -392,12 +398,16 @@ public String toString() {
392398
@SuppressWarnings("unused")
393399
private static class TestController {
394400

401+
@ResponseStatus(code = HttpStatus.CREATED)
395402
String string() { return null; }
396403

404+
@ResponseStatus(HttpStatus.NO_CONTENT)
397405
View view() { return null; }
398406

407+
@ResponseStatus(HttpStatus.PARTIAL_CONTENT)
399408
Mono<String> monoString() { return null; }
400409

410+
@ResponseStatus(code = HttpStatus.SEE_OTHER)
401411
Mono<View> monoView() { return null; }
402412

403413
Mono<Void> monoVoid() { return null; }

0 commit comments

Comments
 (0)