Skip to content

Commit 0a48936

Browse files
committed
ContentNegotiatingViewResolver properly handles invalid accept headers (SPR-7712)
1 parent 3fb753b commit 0a48936

File tree

2 files changed

+93
-91
lines changed

2 files changed

+93
-91
lines changed

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

Lines changed: 80 additions & 81 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.
@@ -29,7 +29,6 @@
2929
import java.util.Set;
3030
import java.util.concurrent.ConcurrentHashMap;
3131
import java.util.concurrent.ConcurrentMap;
32-
3332
import javax.activation.FileTypeMap;
3433
import javax.activation.MimetypesFileTypeMap;
3534
import javax.servlet.ServletContext;
@@ -38,6 +37,7 @@
3837

3938
import org.apache.commons.logging.Log;
4039
import org.apache.commons.logging.LogFactory;
40+
4141
import org.springframework.beans.factory.BeanFactoryUtils;
4242
import org.springframework.core.OrderComparator;
4343
import org.springframework.core.Ordered;
@@ -70,17 +70,17 @@
7070
* <p>This view resolver uses the requested {@linkplain MediaType media type} to select a suitable {@link View} for a
7171
* request. This media type is determined by using the following criteria:
7272
* <ol>
73-
* <li>If the requested path has a file extension and if the {@link #setFavorPathExtension(boolean)} property is
73+
* <li>If the requested path has a file extension and if the {@link #setFavorPathExtension} property is
7474
* {@code true}, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.</li>
75-
* <li>If the request contains a parameter defining the extension and if the {@link #setFavorParameter(boolean)}
75+
* <li>If the request contains a parameter defining the extension and if the {@link #setFavorParameter}
7676
* property is <code>true</code>, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching
7777
* media type. The default name of the parameter is <code>format</code> and it can be configured using the
7878
* {@link #setParameterName(String) parameterName} property.</li>
7979
* <li>If there is no match in the {@link #setMediaTypes(Map) mediaTypes} property and if the Java Activation
80-
* Framework (JAF) is both {@linkplain #setUseJaf(boolean) enabled} and present on the class path,
80+
* Framework (JAF) is both {@linkplain #setUseJaf enabled} and present on the class path,
8181
* {@link FileTypeMap#getContentType(String)} is used instead.</li>
8282
* <li>If the previous steps did not result in a media type, and
83-
* {@link #setIgnoreAcceptHeader(boolean) ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is
83+
* {@link #setIgnoreAcceptHeader ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is
8484
* used.</li>
8585
* </ol>
8686
*
@@ -149,7 +149,7 @@ public int getOrder() {
149149
}
150150

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

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

173173
/**
174-
* Sets the parameter name that can be used to determine the requested media type if the {@link
175-
* #setFavorParameter(boolean)} property is {@code true}. The default parameter name is {@code format}.
174+
* Set the parameter name that can be used to determine the requested media type if the {@link
175+
* #setFavorParameter} property is {@code true}. The default parameter name is {@code format}.
176176
*/
177177
public void setParameterName(String parameterName) {
178178
this.parameterName = parameterName;
179179
}
180180

181181
/**
182-
* Indicates whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
183-
* If set to {@code true}, this view resolver will only refer to the file extension and/or paramter,
184-
* as indicated by the {@link #setFavorPathExtension(boolean) favorPathExtension} and
185-
* {@link #setFavorParameter(boolean) favorParameter} properties.
182+
* Indicate whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
183+
* <p>If set to {@code true}, this view resolver will only refer to the file extension and/or
184+
* parameter, as indicated by the {@link #setFavorPathExtension favorPathExtension} and
185+
* {@link #setFavorParameter favorParameter} properties.
186186
*/
187187
public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
188188
this.ignoreAcceptHeader = ignoreAcceptHeader;
189189
}
190190

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

205204
/**
206-
* Sets the mapping from file extensions to media types.
205+
* Set the mapping from file extensions to media types.
207206
* <p>When this mapping is not set or when an extension is not present, this view resolver
208207
* will fall back to using a {@link FileTypeMap} when the Java Action Framework is available.
209208
*/
@@ -217,15 +216,15 @@ public void setMediaTypes(Map<String, String> mediaTypes) {
217216
}
218217

219218
/**
220-
* Sets the default views to use when a more specific view can not be obtained
219+
* Set the default views to use when a more specific view can not be obtained
221220
* from the {@link ViewResolver} chain.
222221
*/
223222
public void setDefaultViews(List<View> defaultViews) {
224223
this.defaultViews = defaultViews;
225224
}
226225

227226
/**
228-
* Sets the default content type.
227+
* Set the default content type.
229228
* <p>This content type will be used when file extension, parameter, nor {@code Accept}
230229
* header define a content-type, either through being disabled or empty.
231230
*/
@@ -234,7 +233,7 @@ public void setDefaultContentType(MediaType defaultContentType) {
234233
}
235234

236235
/**
237-
* Indicates whether to use the Java Activation Framework to map from file extensions to media types.
236+
* Indicate whether to use the Java Activation Framework to map from file extensions to media types.
238237
* <p>Default is {@code true}, i.e. the Java Activation Framework is used (if available).
239238
*/
240239
public void setUseJaf(boolean useJaf) {
@@ -252,10 +251,8 @@ public void setViewResolvers(List<ViewResolver> viewResolvers) {
252251

253252
@Override
254253
protected void initServletContext(ServletContext servletContext) {
255-
256254
Collection<ViewResolver> matchingBeans =
257-
BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class).values();
258-
255+
BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class).values();
259256
if (this.viewResolvers == null) {
260257
this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
261258
for (ViewResolver viewResolver : matchingBeans) {
@@ -281,13 +278,38 @@ protected void initServletContext(ServletContext servletContext) {
281278
OrderComparator.sort(this.viewResolvers);
282279
}
283280

281+
public View resolveViewName(String viewName, Locale locale) throws Exception {
282+
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
283+
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
284+
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
285+
if (requestedMediaTypes != null) {
286+
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
287+
View bestView = getBestView(candidateViews, requestedMediaTypes);
288+
if (bestView != null) {
289+
return bestView;
290+
}
291+
}
292+
if (this.useNotAcceptableStatusCode) {
293+
if (logger.isDebugEnabled()) {
294+
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
295+
}
296+
return NOT_ACCEPTABLE_VIEW;
297+
}
298+
else {
299+
if (logger.isDebugEnabled()) {
300+
logger.debug("No acceptable view found; returning null");
301+
}
302+
return null;
303+
}
304+
}
305+
284306
/**
285307
* Determines the list of {@link MediaType} for the given {@link HttpServletRequest}.
286308
* <p>The default implementation invokes {@link #getMediaTypeFromFilename(String)} if {@linkplain
287-
* #setFavorPathExtension(boolean) favorPathExtension} property is <code>true</code>. If the property is
288-
* <code>false</code>, or when a media type cannot be determined from the request path, this method will
289-
* inspect the {@code Accept} header of the request.
290-
* <p>This method can be overriden to provide a different algorithm.
309+
* #setFavorPathExtension favorPathExtension} property is <code>true</code>. If the property is
310+
* <code>false</code>, or when a media type cannot be determined from the request path,
311+
* this method will inspect the {@code Accept} header of the request.
312+
* <p>This method can be overridden to provide a different algorithm.
291313
* @param request the current servlet request
292314
* @return the list of media types requested, if any
293315
*/
@@ -319,26 +341,29 @@ protected List<MediaType> getMediaTypes(HttpServletRequest request) {
319341
if (!this.ignoreAcceptHeader) {
320342
String acceptHeader = request.getHeader(ACCEPT_HEADER);
321343
if (StringUtils.hasText(acceptHeader)) {
322-
List<MediaType> acceptableMediaTypes = MediaType.parseMediaTypes(acceptHeader);
323-
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
324-
325-
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
326-
for (MediaType a : acceptableMediaTypes) {
327-
for (MediaType p : producibleMediaTypes) {
328-
if (a.isCompatibleWith(p)) {
329-
compatibleMediaTypes.add(getMostSpecificMediaType(a, p));
344+
try {
345+
List<MediaType> acceptableMediaTypes = MediaType.parseMediaTypes(acceptHeader);
346+
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
347+
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
348+
for (MediaType acceptable : acceptableMediaTypes) {
349+
for (MediaType producible : producibleMediaTypes) {
350+
if (acceptable.isCompatibleWith(producible)) {
351+
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
352+
}
330353
}
331354
}
355+
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
356+
MediaType.sortByQualityValue(mediaTypes);
357+
if (logger.isDebugEnabled()) {
358+
logger.debug("Requested media types are " + mediaTypes + " based on Accept header types " +
359+
"and producible media types " + producibleMediaTypes + ")");
360+
}
361+
return mediaTypes;
332362
}
333-
334-
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
335-
MediaType.sortByQualityValue(mediaTypes);
336-
337-
if (logger.isDebugEnabled()) {
338-
logger.debug("Requested media types are " + mediaTypes + " based on Accept header types " +
339-
"and producible media types " + producibleMediaTypes + ")");
363+
catch (IllegalArgumentException ex) {
364+
logger.debug("Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage());
365+
return null;
340366
}
341-
return mediaTypes;
342367
}
343368
}
344369
if (this.defaultContentType != null) {
@@ -355,7 +380,8 @@ protected List<MediaType> getMediaTypes(HttpServletRequest request) {
355380

356381
@SuppressWarnings("unchecked")
357382
private List<MediaType> getProducibleMediaTypes(HttpServletRequest request) {
358-
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
383+
Set<MediaType> mediaTypes = (Set<MediaType>)
384+
request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
359385
if (!CollectionUtils.isEmpty(mediaTypes)) {
360386
return new ArrayList<MediaType>(mediaTypes);
361387
}
@@ -412,31 +438,6 @@ protected MediaType getMediaTypeFromParameter(String parameterValue) {
412438
return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH));
413439
}
414440

415-
public View resolveViewName(String viewName, Locale locale) throws Exception {
416-
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
417-
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
418-
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
419-
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
420-
View bestView = getBestView(candidateViews, requestedMediaTypes);
421-
if (bestView != null) {
422-
return bestView;
423-
}
424-
else {
425-
if (this.useNotAcceptableStatusCode) {
426-
if (logger.isDebugEnabled()) {
427-
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
428-
}
429-
return NOT_ACCEPTABLE_VIEW;
430-
}
431-
else {
432-
if (logger.isDebugEnabled()) {
433-
logger.debug("No acceptable view found; returning null");
434-
}
435-
return null;
436-
}
437-
}
438-
}
439-
440441
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
441442
throws Exception {
442443

@@ -466,7 +467,7 @@ private List<View> getCandidateViews(String viewName, Locale locale, List<MediaT
466467

467468
private List<String> getExtensionsForMediaType(MediaType requestedMediaType) {
468469
List<String> result = new ArrayList<String>();
469-
for (Entry<String, MediaType> entry : mediaTypes.entrySet()) {
470+
for (Entry<String, MediaType> entry : this.mediaTypes.entrySet()) {
470471
if (requestedMediaType.includes(entry.getValue())) {
471472
result.add(entry.getKey());
472473
}
@@ -490,9 +491,8 @@ private View getBestView(List<View> candidateViews, List<MediaType> requestedMed
490491
}
491492
if (bestView != null) {
492493
if (logger.isDebugEnabled()) {
493-
logger.debug(
494-
"Returning [" + bestView + "] based on requested media type '" + bestRequestedMediaType +
495-
"'");
494+
logger.debug("Returning [" + bestView + "] based on requested media type '" +
495+
bestRequestedMediaType + "'");
496496
}
497497
break;
498498
}
@@ -547,7 +547,7 @@ private static FileTypeMap loadFileTypeMapFromContextSupportModule() {
547547

548548
public static MediaType getMediaType(String fileName) {
549549
String mediaType = fileTypeMap.getContentType(fileName);
550-
return StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null;
550+
return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null);
551551
}
552552
}
553553

@@ -558,8 +558,7 @@ public String getContentType() {
558558
return null;
559559
}
560560

561-
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
562-
throws Exception {
561+
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
563562
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
564563
}
565564
};

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

Lines changed: 13 additions & 10 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.
@@ -16,15 +16,6 @@
1616

1717
package org.springframework.web.servlet.view;
1818

19-
import static org.easymock.EasyMock.createMock;
20-
import static org.easymock.EasyMock.expect;
21-
import static org.easymock.EasyMock.replay;
22-
import static org.easymock.EasyMock.verify;
23-
import static org.junit.Assert.assertEquals;
24-
import static org.junit.Assert.assertNotNull;
25-
import static org.junit.Assert.assertNull;
26-
import static org.junit.Assert.assertSame;
27-
2819
import java.util.ArrayList;
2920
import java.util.Arrays;
3021
import java.util.Collections;
@@ -37,6 +28,7 @@
3728
import org.junit.After;
3829
import org.junit.Before;
3930
import org.junit.Test;
31+
4032
import org.springframework.http.MediaType;
4133
import org.springframework.mock.web.MockHttpServletRequest;
4234
import org.springframework.mock.web.MockHttpServletResponse;
@@ -48,6 +40,9 @@
4840
import org.springframework.web.servlet.View;
4941
import org.springframework.web.servlet.ViewResolver;
5042

43+
import static org.easymock.EasyMock.*;
44+
import static org.junit.Assert.*;
45+
5146
/**
5247
* @author Arjen Poutsma
5348
*/
@@ -191,6 +186,14 @@ public void resolveViewNameWithAcceptHeader() throws Exception {
191186
verify(viewResolverMock, viewMock);
192187
}
193188

189+
@Test
190+
public void resolveViewNameWithInvalidAcceptHeader() throws Exception {
191+
request.addHeader("Accept", "application");
192+
193+
View result = viewResolver.resolveViewName("test", Locale.ENGLISH);
194+
assertNull(result);
195+
}
196+
194197
@Test
195198
public void resolveViewNameWithRequestParameter() throws Exception {
196199
request.addParameter("format", "xls");

0 commit comments

Comments
 (0)