Skip to content

Commit b11970e

Browse files
committed
SPR-5923 - HttpMessageConverter selection as a result of @responsebody should consider the requested content type
1 parent de234e6 commit b11970e

File tree

6 files changed

+182
-24
lines changed

6 files changed

+182
-24
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java

+30-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.OutputStream;
2121
import java.io.Reader;
2222
import java.io.Writer;
23+
import java.io.IOException;
2324
import java.lang.reflect.Method;
2425
import java.security.Principal;
2526
import java.util.ArrayList;
@@ -32,6 +33,7 @@
3233
import java.util.Locale;
3334
import java.util.Map;
3435
import java.util.Set;
36+
import java.util.Iterator;
3537
import java.util.concurrent.ConcurrentHashMap;
3638
import javax.servlet.ServletException;
3739
import javax.servlet.ServletRequest;
@@ -51,6 +53,7 @@
5153
import org.springframework.core.annotation.AnnotationUtils;
5254
import org.springframework.http.HttpInputMessage;
5355
import org.springframework.http.HttpOutputMessage;
56+
import org.springframework.http.MediaType;
5457
import org.springframework.http.converter.BufferedImageHttpMessageConverter;
5558
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
5659
import org.springframework.http.converter.FormHttpMessageConverter;
@@ -71,6 +74,7 @@
7174
import org.springframework.validation.support.BindingAwareModelMap;
7275
import org.springframework.web.HttpRequestMethodNotSupportedException;
7376
import org.springframework.web.HttpSessionRequiredException;
77+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
7478
import org.springframework.web.bind.MissingServletRequestParameterException;
7579
import org.springframework.web.bind.ServletRequestDataBinder;
7680
import org.springframework.web.bind.WebDataBinder;
@@ -726,15 +730,7 @@ public ModelAndView getModelAndView(Method handlerMethod,
726730
}
727731

728732
if (returnValue != null && handlerMethod.isAnnotationPresent(ResponseBody.class)) {
729-
Class returnValueType = returnValue.getClass();
730-
HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
731-
for (HttpMessageConverter messageConverter : messageConverters) {
732-
if (messageConverter.supports(returnValueType)) {
733-
messageConverter.write(returnValue, outputMessage);
734-
responseArgumentUsed = true;
735-
return null;
736-
}
737-
}
733+
handleRequestBody(returnValue, webRequest);
738734
}
739735

740736
if (returnValue instanceof ModelAndView) {
@@ -777,6 +773,31 @@ else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) {
777773
throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
778774
}
779775
}
776+
777+
@SuppressWarnings("unchecked")
778+
private void handleRequestBody(Object returnValue, ServletWebRequest webRequest) throws ServletException, IOException {
779+
HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest());
780+
List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
781+
HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
782+
Class<?> returnValueType = returnValue.getClass();
783+
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
784+
for (HttpMessageConverter messageConverter : messageConverters) {
785+
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
786+
if (messageConverter.supports(returnValueType)) {
787+
for (Object o : messageConverter.getSupportedMediaTypes()) {
788+
MediaType supportedMediaType = (MediaType) o;
789+
for (MediaType acceptedMediaType : acceptedMediaTypes) {
790+
if (supportedMediaType.includes(acceptedMediaType)) {
791+
messageConverter.write(returnValue, outputMessage);
792+
responseArgumentUsed = true;
793+
return;
794+
}
795+
}
796+
}
797+
}
798+
}
799+
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
800+
}
780801
}
781802

782803
static class RequestMappingInfo {

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.util.StringUtils;
3434
import org.springframework.web.HttpMediaTypeNotSupportedException;
3535
import org.springframework.web.HttpRequestMethodNotSupportedException;
36+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
3637
import org.springframework.web.bind.MissingServletRequestParameterException;
3738
import org.springframework.web.servlet.ModelAndView;
3839
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
@@ -93,6 +94,10 @@ else if (ex instanceof HttpMediaTypeNotSupportedException) {
9394
return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response,
9495
handler);
9596
}
97+
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
98+
return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response,
99+
handler);
100+
}
96101
else if (ex instanceof MissingServletRequestParameterException) {
97102
return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request,
98103
response, handler);
@@ -169,7 +174,7 @@ protected ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotS
169174

170175
/**
171176
* Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}
172-
* were found for the PUT or POSTed content. <p>The default implementation sends an HTTP 415 error, sets the "Allow"
177+
* were found for the PUT or POSTed content. <p>The default implementation sends an HTTP 415 error, sets the "Accept"
173178
* header, and returns an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen, or the
174179
* HttpMediaTypeNotSupportedException could be rethrown as-is.
175180
*
@@ -194,6 +199,29 @@ protected ModelAndView handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupported
194199
return new ModelAndView();
195200
}
196201

202+
/**
203+
* Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}
204+
* were found that were acceptable for the client (expressed via the {@code Accept} header.
205+
* <p>The default implementation sends an HTTP 406 error and returns an empty {@code ModelAndView}. Alternatively,
206+
* a fallback view could be chosen, or the HttpMediaTypeNotAcceptableException could be rethrown as-is.
207+
*
208+
* @param ex the HttpMediaTypeNotAcceptableException to be handled
209+
* @param request current HTTP request
210+
* @param response current HTTP response
211+
* @param handler the executed handler, or <code>null</code> if none chosen at the time of the exception (for example,
212+
* if multipart resolution failed)
213+
* @return a ModelAndView to render, or <code>null</code> if handled directly
214+
* @throws Exception an Exception that should be thrown as result of the servlet request
215+
*/
216+
protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
217+
HttpServletRequest request,
218+
HttpServletResponse response,
219+
Object handler) throws Exception {
220+
221+
response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
222+
return new ModelAndView();
223+
}
224+
197225
/**
198226
* Handle the case when a required parameter is missing. <p>The default implementation sends an HTTP 400 error, and
199227
* returns an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen, or the

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java

+15
Original file line numberDiff line numberDiff line change
@@ -876,11 +876,26 @@ public void requestBodyResponseBody() throws ServletException, IOException {
876876
String requestBody = "Hello World";
877877
request.setContent(requestBody.getBytes("UTF-8"));
878878
request.addHeader("Content-Type", "text/plain; charset=utf-8");
879+
request.addHeader("Accept", "text/*");
879880
MockHttpServletResponse response = new MockHttpServletResponse();
880881
servlet.service(request, response);
881882
assertEquals(requestBody, response.getContentAsString());
882883
}
883884

885+
@Test
886+
public void responseBodyNoAcceptableMediaType() throws ServletException, IOException {
887+
initServlet(RequestBodyController.class);
888+
889+
MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something");
890+
String requestBody = "Hello World";
891+
request.setContent(requestBody.getBytes("UTF-8"));
892+
request.addHeader("Content-Type", "text/plain; charset=utf-8");
893+
request.addHeader("Accept", "application/pdf, application/msword");
894+
MockHttpServletResponse response = new MockHttpServletResponse();
895+
servlet.service(request, response);
896+
assertEquals(406, response.getStatus());
897+
}
898+
884899
@Test
885900
public void unsupportedRequestBody() throws ServletException, IOException {
886901
initServlet(RequestBodyController.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2009 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web;
18+
19+
import java.util.List;
20+
import java.util.Collections;
21+
import javax.servlet.ServletException;
22+
23+
import org.springframework.http.MediaType;
24+
25+
/**
26+
* Abstract base for exceptions related to media types. Adds a list of supported {@link MediaType MediaTypes}.
27+
*
28+
* @author Arjen Poutsma
29+
* @since 3.0
30+
*/
31+
public abstract class HttpMediaTypeException extends ServletException {
32+
33+
private final List<MediaType> supportedMediaTypes;
34+
35+
/**
36+
* Create a new MediaTypeException.
37+
* @param message the exception message
38+
*/
39+
protected HttpMediaTypeException(String message) {
40+
super(message);
41+
this.supportedMediaTypes = Collections.emptyList();
42+
}
43+
44+
/**
45+
* Create a new HttpMediaTypeNotSupportedException.
46+
* @param supportedMediaTypes the list of supported media types
47+
*/
48+
protected HttpMediaTypeException(String message, List<MediaType> supportedMediaTypes) {
49+
super(message);
50+
this.supportedMediaTypes = supportedMediaTypes;
51+
}
52+
53+
/**
54+
* Return the list of supported media types.
55+
*/
56+
public List<MediaType> getSupportedMediaTypes() {
57+
return supportedMediaTypes;
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2009 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web;
18+
19+
import java.util.List;
20+
21+
import org.springframework.http.MediaType;
22+
23+
/**
24+
* Exception thrown when the request handler cannot generate a response that is acceptable by the client.
25+
*
26+
* @author Arjen Poutsma
27+
* @since 3.0
28+
*/
29+
public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException {
30+
31+
/**
32+
* Create a new HttpMediaTypeNotAcceptableException.
33+
* @param message the exception message
34+
*/
35+
public HttpMediaTypeNotAcceptableException(String message) {
36+
super(message);
37+
}
38+
39+
/**
40+
* Create a new HttpMediaTypeNotSupportedException.
41+
* @param supportedMediaTypes the list of supported media types
42+
*/
43+
public HttpMediaTypeNotAcceptableException(List<MediaType> supportedMediaTypes) {
44+
super("Could not find acceptable representation", supportedMediaTypes);
45+
}
46+
47+
}

org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java

+2-14
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
package org.springframework.web;
1818

19-
import java.util.Collections;
2019
import java.util.List;
21-
import javax.servlet.ServletException;
2220

2321
import org.springframework.http.MediaType;
2422

@@ -29,20 +27,17 @@
2927
* @author Arjen Poutsma
3028
* @since 3.0
3129
*/
32-
public class HttpMediaTypeNotSupportedException extends ServletException {
30+
public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
3331

3432
private final MediaType contentType;
3533

36-
private final List<MediaType> supportedMediaTypes;
37-
3834
/**
3935
* Create a new HttpMediaTypeNotSupportedException.
4036
* @param message the exception message
4137
*/
4238
public HttpMediaTypeNotSupportedException(String message) {
4339
super(message);
4440
this.contentType = null;
45-
this.supportedMediaTypes = Collections.emptyList();
4641
}
4742

4843
/**
@@ -61,9 +56,8 @@ public HttpMediaTypeNotSupportedException(MediaType contentType, List<MediaType>
6156
* @param msg the detail message
6257
*/
6358
public HttpMediaTypeNotSupportedException(MediaType contentType, List<MediaType> supportedMediaTypes, String msg) {
64-
super(msg);
59+
super(msg, supportedMediaTypes);
6560
this.contentType = contentType;
66-
this.supportedMediaTypes = supportedMediaTypes;
6761
}
6862

6963
/**
@@ -73,10 +67,4 @@ public MediaType getContentType() {
7367
return contentType;
7468
}
7569

76-
/**
77-
* Return the list of supported media types.
78-
*/
79-
public List<MediaType> getSupportedMediaTypes() {
80-
return supportedMediaTypes;
81-
}
8270
}

0 commit comments

Comments
 (0)