Skip to content

Commit 25aef4d

Browse files
committed
ResponseStatusException reason is optional (with lazily constructed message)
Issue: SPR-15524
1 parent edbf9fa commit 25aef4d

File tree

6 files changed

+69
-52
lines changed

6 files changed

+69
-52
lines changed

spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.server;
1818

19+
import org.springframework.core.NestedExceptionUtils;
1920
import org.springframework.core.NestedRuntimeException;
2021
import org.springframework.http.HttpStatus;
2122
import org.springframework.util.Assert;
@@ -24,6 +25,7 @@
2425
* Base class for exceptions associated with specific HTTP response status codes.
2526
*
2627
* @author Rossen Stoyanchev
28+
* @author Juergen Hoeller
2729
* @since 5.0
2830
*/
2931
@SuppressWarnings("serial")
@@ -35,37 +37,56 @@ public class ResponseStatusException extends NestedRuntimeException {
3537

3638

3739
/**
38-
* Constructor with a response code and a reason to add to the exception
40+
* Constructor with a response status.
41+
* @param status the HTTP status (required)
42+
*/
43+
public ResponseStatusException(HttpStatus status) {
44+
this(status, null, null);
45+
}
46+
47+
/**
48+
* Constructor with a response status and a reason to add to the exception
3949
* message as explanation.
50+
* @param status the HTTP status (required)
51+
* @param reason the associated reason (optional)
4052
*/
4153
public ResponseStatusException(HttpStatus status, String reason) {
4254
this(status, reason, null);
4355
}
4456

4557
/**
46-
* Constructor with a nested exception.
58+
* Constructor with a response status and a reason to add to the exception
59+
* message as explanation, as well as a nested exception.
60+
* @param status the HTTP status (required)
61+
* @param reason the associated reason (optional)
62+
* @param cause a nested exception (optional)
4763
*/
4864
public ResponseStatusException(HttpStatus status, String reason, Throwable cause) {
49-
super("Request failure [status: " + status + ", reason: \"" + reason + "\"]", cause);
50-
Assert.notNull(status, "'status' is required");
51-
Assert.notNull(reason, "'reason' is required");
65+
super(null, cause);
66+
Assert.notNull(status, "HttpStatus is required");
5267
this.status = status;
5368
this.reason = reason;
5469
}
5570

5671

5772
/**
58-
* The HTTP status that fits the exception.
73+
* The HTTP status that fits the exception (never {@code null}).
5974
*/
6075
public HttpStatus getStatus() {
6176
return this.status;
6277
}
6378

6479
/**
65-
* The reason explaining the exception.
80+
* The reason explaining the exception (potentially {@code null} or empty).
6681
*/
6782
public String getReason() {
6883
return this.reason;
6984
}
7085

86+
@Override
87+
public String getMessage() {
88+
String msg = "Response status " + this.status + (this.reason != null ? " with reason \"" + reason + "\"" : "");
89+
return NestedExceptionUtils.buildMessage(msg, getCause());
90+
}
91+
7192
}

spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@
5151
import static org.hamcrest.CoreMatchers.instanceOf;
5252
import static org.hamcrest.CoreMatchers.startsWith;
5353
import static org.hamcrest.Matchers.is;
54-
import static org.junit.Assert.assertEquals;
55-
import static org.junit.Assert.assertSame;
56-
import static org.junit.Assert.assertThat;
57-
import static org.springframework.http.MediaType.APPLICATION_JSON;
54+
import static org.junit.Assert.*;
55+
import static org.springframework.http.MediaType.*;
5856

5957
/**
6058
* Test the effect of exceptions at different stages of request processing by
@@ -87,8 +85,7 @@ public void noHandler() throws Exception {
8785
StepVerifier.create(publisher)
8886
.consumeErrorWith(error -> {
8987
assertThat(error, instanceOf(ResponseStatusException.class));
90-
assertThat(error.getMessage(),
91-
is("Request failure [status: 404, reason: \"No matching handler\"]"));
88+
assertThat(error.getMessage(), is("Response status 404 with reason \"No matching handler\""));
9289
})
9390
.verify();
9491
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,11 @@
3232
import org.springframework.web.reactive.HandlerResult;
3333
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
3434

35-
import static org.hamcrest.Matchers.is;
36-
import static org.junit.Assert.assertEquals;
37-
import static org.junit.Assert.assertThat;
38-
import static org.junit.Assert.assertTrue;
39-
import static org.junit.Assert.fail;
35+
import static org.hamcrest.Matchers.*;
36+
import static org.junit.Assert.*;
4037
import static org.mockito.Mockito.any;
41-
import static org.mockito.Mockito.mock;
42-
import static org.mockito.Mockito.when;
43-
import static org.springframework.web.method.ResolvableMethod.on;
38+
import static org.mockito.Mockito.*;
39+
import static org.springframework.web.method.ResolvableMethod.*;
4440

4541
/**
4642
* Unit tests for {@link InvocableHandlerMethod}.
@@ -109,7 +105,7 @@ public void resolverThrowsException() throws Exception {
109105
fail("Expected UnsupportedMediaTypeStatusException");
110106
}
111107
catch (UnsupportedMediaTypeStatusException ex) {
112-
assertThat(ex.getMessage(), is("Request failure [status: 415, reason: \"boo\"]"));
108+
assertThat(ex.getMessage(), is("Response status 415 with reason \"boo\""));
113109
}
114110
}
115111

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,21 @@
4848
import org.springframework.web.reactive.BindingContext;
4949
import org.springframework.web.reactive.HandlerMapping;
5050
import org.springframework.web.reactive.HandlerResult;
51-
import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration;
51+
import org.springframework.web.reactive.result.method.RequestMappingInfo.*;
5252
import org.springframework.web.server.MethodNotAllowedException;
5353
import org.springframework.web.server.NotAcceptableStatusException;
5454
import org.springframework.web.server.ServerWebExchange;
5555
import org.springframework.web.server.ServerWebInputException;
5656
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
5757
import org.springframework.web.server.support.HttpRequestPathHelper;
5858

59-
import static org.hamcrest.CoreMatchers.containsString;
59+
import static org.hamcrest.CoreMatchers.*;
6060
import static org.junit.Assert.*;
61-
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get;
62-
import static org.springframework.web.bind.annotation.RequestMethod.GET;
63-
import static org.springframework.web.bind.annotation.RequestMethod.HEAD;
64-
import static org.springframework.web.bind.annotation.RequestMethod.OPTIONS;
65-
import static org.springframework.web.method.MvcAnnotationPredicates.getMapping;
66-
import static org.springframework.web.method.MvcAnnotationPredicates.requestMapping;
67-
import static org.springframework.web.method.ResolvableMethod.on;
68-
import static org.springframework.web.reactive.result.method.RequestMappingInfo.paths;
61+
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.*;
62+
import static org.springframework.web.bind.annotation.RequestMethod.*;
63+
import static org.springframework.web.method.MvcAnnotationPredicates.*;
64+
import static org.springframework.web.method.ResolvableMethod.*;
65+
import static org.springframework.web.reactive.result.method.RequestMappingInfo.*;
6966

7067
/**
7168
* Unit tests for {@link RequestMappingInfoHandlerMapping}.
@@ -165,8 +162,7 @@ public void getHandlerTestInvalidContentType() throws Exception {
165162
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
166163

167164
assertError(mono, UnsupportedMediaTypeStatusException.class,
168-
ex -> assertEquals("Request failure [status: 415, " +
169-
"reason: \"Invalid mime type \"bogus\": does not contain '/'\"]",
165+
ex -> assertEquals("Response status 415 with reason \"Invalid mime type \"bogus\": does not contain '/'\"",
170166
ex.getMessage()));
171167
}
172168

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,7 +49,8 @@
4949
* @author Rossen Stoyanchev
5050
* @author Sam Brannen
5151
* @since 3.0
52-
* @see AnnotatedElementUtils#findMergedAnnotation
52+
* @see ResponseStatus
53+
* @see ResponseStatusException
5354
*/
5455
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
5556

@@ -99,9 +100,8 @@ protected ModelAndView doResolveException(HttpServletRequest request, HttpServle
99100
* @param ex the exception
100101
* @return an empty ModelAndView, i.e. exception resolved
101102
*/
102-
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus,
103-
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
104-
throws Exception {
103+
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
104+
HttpServletResponse response, Object handler, Exception ex) throws Exception {
105105

106106
int statusCode = responseStatus.code().value();
107107
String reason = responseStatus.reason();
@@ -125,8 +125,7 @@ protected ModelAndView resolveResponseStatusException(ResponseStatusException ex
125125

126126
int statusCode = ex.getStatus().value();
127127
String reason = ex.getReason();
128-
applyStatusAndReason(statusCode, reason, response);
129-
return new ModelAndView();
128+
return applyStatusAndReason(statusCode, reason, response);
130129
}
131130

132131
/**
@@ -135,19 +134,22 @@ protected ModelAndView resolveResponseStatusException(ResponseStatusException ex
135134
* {@link HttpServletResponse#sendError(int)} or
136135
* {@link HttpServletResponse#sendError(int, String)} if there is a reason
137136
* and then returns an empty ModelAndView.
137+
* @param statusCode the HTTP status code
138+
* @param reason the associated reason (may be {@code null} or empty)
139+
* @param response current HTTP response
138140
* @since 5.0
139141
*/
140142
protected ModelAndView applyStatusAndReason(int statusCode, String reason, HttpServletResponse response)
141143
throws IOException {
142144

143-
if (this.messageSource != null) {
144-
reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale());
145-
}
146145
if (!StringUtils.hasLength(reason)) {
147146
response.sendError(statusCode);
148147
}
149148
else {
150-
response.sendError(statusCode, reason);
149+
String resolvedReason = (this.messageSource != null ?
150+
this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
151+
reason);
152+
response.sendError(statusCode, resolvedReason);
151153
}
152154
return new ModelAndView();
153155
}

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -111,23 +111,28 @@ public void nestedException() throws Exception {
111111
Exception cause = new StatusCodeAndReasonMessageException();
112112
TypeMismatchException ex = new TypeMismatchException("value", ITestBean.class, cause);
113113
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
114-
assertResolved(mav, 410, null);
114+
assertResolved(mav, 410, "gone.reason");
115115
}
116116

117117
@Test
118118
public void responseStatusException() throws Exception {
119-
ResponseStatusException ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, "The reason");
119+
ResponseStatusException ex = new ResponseStatusException(HttpStatus.BAD_REQUEST);
120120
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
121121
assertResolved(mav, 400, null);
122122
}
123123

124+
@Test // SPR-15524
125+
public void responseStatusExceptionWithReason() throws Exception {
126+
ResponseStatusException ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, "The reason");
127+
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
128+
assertResolved(mav, 400, "The reason");
129+
}
130+
124131

125132
private void assertResolved(ModelAndView mav, int status, String reason) {
126133
assertTrue("No Empty ModelAndView returned", mav != null && mav.isEmpty());
127134
assertEquals(status, response.getStatus());
128-
if (reason != null) {
129-
assertEquals(reason, response.getErrorMessage());
130-
}
135+
assertEquals(reason, response.getErrorMessage());
131136
assertTrue(response.isCommitted());
132137
}
133138

0 commit comments

Comments
 (0)