Skip to content

Commit cb0c0c5

Browse files
committed
ContentNegotiatingViewResolver properly handles invalid accept headers (SPR-7712)
1 parent 215ccc2 commit cb0c0c5

File tree

2 files changed

+82
-68
lines changed

2 files changed

+82
-68
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java

Lines changed: 70 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2010 the original author or authors.
2+
* Copyright 2002-2011 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.
@@ -66,17 +66,17 @@
6666
* <p>This view resolver uses the requested {@linkplain MediaType media type} to select a suitable {@link View} for a
6767
* request. This media type is determined by using the following criteria:
6868
* <ol>
69-
* <li>If the requested path has a file extension and if the {@link #setFavorPathExtension(boolean)} property is
69+
* <li>If the requested path has a file extension and if the {@link #setFavorPathExtension} property is
7070
* {@code true}, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.</li>
71-
* <li>If the request contains a parameter defining the extension and if the {@link #setFavorParameter(boolean)}
71+
* <li>If the request contains a parameter defining the extension and if the {@link #setFavorParameter}
7272
* property is <code>true</code>, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching
7373
* media type. The default name of the parameter is <code>format</code> and it can be configured using the
7474
* {@link #setParameterName(String) parameterName} property.</li>
7575
* <li>If there is no match in the {@link #setMediaTypes(Map) mediaTypes} property and if the Java Activation
76-
* Framework (JAF) is both {@linkplain #setUseJaf(boolean) enabled} and present on the class path,
76+
* Framework (JAF) is both {@linkplain #setUseJaf enabled} and present on the class path,
7777
* {@link FileTypeMap#getContentType(String)} is used instead.</li>
7878
* <li>If the previous steps did not result in a media type, and
79-
* {@link #setIgnoreAcceptHeader(boolean) ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is
79+
* {@link #setIgnoreAcceptHeader ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is
8080
* used.</li>
8181
* </ol>
8282
*
@@ -145,7 +145,7 @@ public int getOrder() {
145145
}
146146

147147
/**
148-
* Indicates whether the extension of the request path should be used to determine the requested media type,
148+
* Indicate whether the extension of the request path should be used to determine the requested media type,
149149
* in favor of looking at the {@code Accept} header. The default value is {@code true}.
150150
* <p>For instance, when this flag is <code>true</code> (the default), a request for {@code /hotels.pdf}
151151
* will result in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the
@@ -156,7 +156,7 @@ public void setFavorPathExtension(boolean favorPathExtension) {
156156
}
157157

158158
/**
159-
* Indicates whether a request parameter should be used to determine the requested media type,
159+
* Indicate whether a request parameter should be used to determine the requested media type,
160160
* in favor of looking at the {@code Accept} header. The default value is {@code false}.
161161
* <p>For instance, when this flag is <code>true</code>, a request for {@code /hotels?format=pdf} will result
162162
* in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined
@@ -167,39 +167,38 @@ public void setFavorParameter(boolean favorParameter) {
167167
}
168168

169169
/**
170-
* Sets the parameter name that can be used to determine the requested media type if the {@link
171-
* #setFavorParameter(boolean)} property is {@code true}. The default parameter name is {@code format}.
170+
* Set the parameter name that can be used to determine the requested media type if the {@link
171+
* #setFavorParameter} property is {@code true}. The default parameter name is {@code format}.
172172
*/
173173
public void setParameterName(String parameterName) {
174174
this.parameterName = parameterName;
175175
}
176176

177177
/**
178-
* Indicates whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
179-
* If set to {@code true}, this view resolver will only refer to the file extension and/or paramter,
180-
* as indicated by the {@link #setFavorPathExtension(boolean) favorPathExtension} and
181-
* {@link #setFavorParameter(boolean) favorParameter} properties.
178+
* Indicate whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
179+
* <p>If set to {@code true}, this view resolver will only refer to the file extension and/or
180+
* parameter, as indicated by the {@link #setFavorPathExtension favorPathExtension} and
181+
* {@link #setFavorParameter favorParameter} properties.
182182
*/
183183
public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
184184
this.ignoreAcceptHeader = ignoreAcceptHeader;
185185
}
186186

187187
/**
188-
* Indicates whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable} status code should be
189-
* returned if no suitable view can be found.
190-
*
188+
* Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable}
189+
* status code should be returned if no suitable view can be found.
191190
* <p>Default is {@code false}, meaning that this view resolver returns {@code null} for
192-
* {@link #resolveViewName(String, Locale)} when an acceptable view cannot be found. This will allow for view
193-
* resolvers chaining. When this property is set to {@code true},
194-
* {@link #resolveViewName(String, Locale)} will respond with a view that sets the response status to
195-
* {@code 406 Not Acceptable} instead.
191+
* {@link #resolveViewName(String, Locale)} when an acceptable view cannot be found.
192+
* This will allow for view resolvers chaining. When this property is set to {@code true},
193+
* {@link #resolveViewName(String, Locale)} will respond with a view that sets the
194+
* response status to {@code 406 Not Acceptable} instead.
196195
*/
197196
public void setUseNotAcceptableStatusCode(boolean useNotAcceptableStatusCode) {
198197
this.useNotAcceptableStatusCode = useNotAcceptableStatusCode;
199198
}
200199

201200
/**
202-
* Sets the mapping from file extensions to media types.
201+
* Set the mapping from file extensions to media types.
203202
* <p>When this mapping is not set or when an extension is not present, this view resolver
204203
* will fall back to using a {@link FileTypeMap} when the Java Action Framework is available.
205204
*/
@@ -213,15 +212,15 @@ public void setMediaTypes(Map<String, String> mediaTypes) {
213212
}
214213

215214
/**
216-
* Sets the default views to use when a more specific view can not be obtained
215+
* Set the default views to use when a more specific view can not be obtained
217216
* from the {@link ViewResolver} chain.
218217
*/
219218
public void setDefaultViews(List<View> defaultViews) {
220219
this.defaultViews = defaultViews;
221220
}
222221

223222
/**
224-
* Sets the default content type.
223+
* Set the default content type.
225224
* <p>This content type will be used when file extension, parameter, nor {@code Accept}
226225
* header define a content-type, either through being disabled or empty.
227226
*/
@@ -230,7 +229,7 @@ public void setDefaultContentType(MediaType defaultContentType) {
230229
}
231230

232231
/**
233-
* Indicates whether to use the Java Activation Framework to map from file extensions to media types.
232+
* Indicate whether to use the Java Activation Framework to map from file extensions to media types.
234233
* <p>Default is {@code true}, i.e. the Java Activation Framework is used (if available).
235234
*/
236235
public void setUseJaf(boolean useJaf) {
@@ -265,13 +264,38 @@ protected void initServletContext(ServletContext servletContext) {
265264
OrderComparator.sort(this.viewResolvers);
266265
}
267266

267+
public View resolveViewName(String viewName, Locale locale) throws Exception {
268+
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
269+
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
270+
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
271+
if (requestedMediaTypes != null) {
272+
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
273+
View bestView = getBestView(candidateViews, requestedMediaTypes);
274+
if (bestView != null) {
275+
return bestView;
276+
}
277+
}
278+
if (this.useNotAcceptableStatusCode) {
279+
if (logger.isDebugEnabled()) {
280+
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
281+
}
282+
return NOT_ACCEPTABLE_VIEW;
283+
}
284+
else {
285+
if (logger.isDebugEnabled()) {
286+
logger.debug("No acceptable view found; returning null");
287+
}
288+
return null;
289+
}
290+
}
291+
268292
/**
269293
* Determines the list of {@link MediaType} for the given {@link HttpServletRequest}.
270294
* <p>The default implementation invokes {@link #getMediaTypeFromFilename(String)} if {@linkplain
271-
* #setFavorPathExtension(boolean) favorPathExtension} property is <code>true</code>. If the property is
272-
* <code>false</code>, or when a media type cannot be determined from the request path, this method will
273-
* inspect the {@code Accept} header of the request.
274-
* <p>This method can be overriden to provide a different algorithm.
295+
* #setFavorPathExtension favorPathExtension} property is <code>true</code>. If the property is
296+
* <code>false</code>, or when a media type cannot be determined from the request path,
297+
* this method will inspect the {@code Accept} header of the request.
298+
* <p>This method can be overridden to provide a different algorithm.
275299
* @param request the current servlet request
276300
* @return the list of media types requested, if any
277301
*/
@@ -303,12 +327,20 @@ protected List<MediaType> getMediaTypes(HttpServletRequest request) {
303327
if (!this.ignoreAcceptHeader) {
304328
String acceptHeader = request.getHeader(ACCEPT_HEADER);
305329
if (StringUtils.hasText(acceptHeader)) {
306-
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
307-
MediaType.sortByQualityValue(mediaTypes);
308-
if (logger.isDebugEnabled()) {
309-
logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)");
330+
try {
331+
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
332+
MediaType.sortByQualityValue(mediaTypes);
333+
if (logger.isDebugEnabled()) {
334+
logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)");
335+
}
336+
return mediaTypes;
337+
}
338+
catch (IllegalArgumentException ex) {
339+
if (logger.isDebugEnabled()) {
340+
logger.debug("Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage());
341+
}
342+
return null;
310343
}
311-
return mediaTypes;
312344
}
313345
}
314346
if (this.defaultContentType != null) {
@@ -360,31 +392,6 @@ protected MediaType getMediaTypeFromParameter(String parameterValue) {
360392
return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH));
361393
}
362394

363-
public View resolveViewName(String viewName, Locale locale) throws Exception {
364-
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
365-
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
366-
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
367-
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
368-
View bestView = getBestView(candidateViews, requestedMediaTypes);
369-
if (bestView != null) {
370-
return bestView;
371-
}
372-
else {
373-
if (this.useNotAcceptableStatusCode) {
374-
if (logger.isDebugEnabled()) {
375-
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
376-
}
377-
return NOT_ACCEPTABLE_VIEW;
378-
}
379-
else {
380-
if (logger.isDebugEnabled()) {
381-
logger.debug("No acceptable view found; returning null");
382-
}
383-
return null;
384-
}
385-
}
386-
}
387-
388395
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
389396
throws Exception {
390397

@@ -414,7 +421,7 @@ private List<View> getCandidateViews(String viewName, Locale locale, List<MediaT
414421

415422
private List<String> getExtensionsForMediaType(MediaType requestedMediaType) {
416423
List<String> result = new ArrayList<String>();
417-
for (Entry<String, MediaType> entry : mediaTypes.entrySet()) {
424+
for (Entry<String, MediaType> entry : this.mediaTypes.entrySet()) {
418425
if (requestedMediaType.includes(entry.getValue())) {
419426
result.add(entry.getKey());
420427
}
@@ -438,9 +445,8 @@ private View getBestView(List<View> candidateViews, List<MediaType> requestedMed
438445
}
439446
if (bestView != null) {
440447
if (logger.isDebugEnabled()) {
441-
logger.debug(
442-
"Returning [" + bestView + "] based on requested media type '" + bestRequestedMediaType +
443-
"'");
448+
logger.debug("Returning [" + bestView + "] based on requested media type '" +
449+
bestRequestedMediaType + "'");
444450
}
445451
break;
446452
}
@@ -495,7 +501,7 @@ private static FileTypeMap loadFileTypeMapFromContextSupportModule() {
495501

496502
public static MediaType getMediaType(String fileName) {
497503
String mediaType = fileTypeMap.getContentType(fileName);
498-
return StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null;
504+
return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null);
499505
}
500506
}
501507

@@ -506,8 +512,7 @@ public String getContentType() {
506512
return null;
507513
}
508514

509-
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
510-
throws Exception {
515+
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
511516
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
512517
}
513518
};

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2010 the original author or authors.
2+
* Copyright 2002-2011 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.
@@ -24,9 +24,7 @@
2424
import java.util.Locale;
2525
import java.util.Map;
2626

27-
import static org.easymock.EasyMock.*;
2827
import org.junit.After;
29-
import static org.junit.Assert.*;
3028
import org.junit.Before;
3129
import org.junit.Test;
3230

@@ -38,6 +36,9 @@
3836
import org.springframework.web.servlet.View;
3937
import org.springframework.web.servlet.ViewResolver;
4038

39+
import static org.easymock.EasyMock.*;
40+
import static org.junit.Assert.*;
41+
4142
/**
4243
* @author Arjen Poutsma
4344
*/
@@ -171,6 +172,14 @@ public void resolveViewNameWithAcceptHeader() throws Exception {
171172
verify(viewResolverMock, viewMock);
172173
}
173174

175+
@Test
176+
public void resolveViewNameWithInvalidAcceptHeader() throws Exception {
177+
request.addHeader("Accept", "application");
178+
179+
View result = viewResolver.resolveViewName("test", Locale.ENGLISH);
180+
assertNull(result);
181+
}
182+
174183
@Test
175184
public void resolveViewNameWithRequestParameter() throws Exception {
176185
request.addParameter("format", "xls");

0 commit comments

Comments
 (0)