Skip to content

Commit fc34b0c

Browse files
committed
Reorder HTTP headers processing in RequestMappingHandlerAdapter
Prior to this change, the `RequestMappingHandlerAdapter` would first add a "Cache-Control" HTTP header to the response (depending on its `WebContentGenerator` configuration and `@SessionAttributes` on the handler class); then, the Adapter would delegate the actual handler the processing of the request. This leads to issues, as the handler does not have full control to the response and has to deal with pre-existing headers in the response. This means that the Adapter and the handler can add incompatible Cache-Control directives without knowing it, since one cannot see the headers added by the other until the response is committed. This commit switches the order of execution: first, the handler is called (possibly adding HTTP headers), then the RMHA processes the response and adds "Cache-Control" directives *only if there's no Cache-Control header already defined*. Issue: SPR-13867 cherry-picked from 8f1d06f
1 parent a4cb3cf commit fc34b0c

File tree

4 files changed

+80
-45
lines changed

4 files changed

+80
-45
lines changed

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -714,27 +714,30 @@ protected boolean supportsInternal(HandlerMethod handlerMethod) {
714714
protected ModelAndView handleInternal(HttpServletRequest request,
715715
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
716716

717+
ModelAndView mav = null;
717718
checkRequest(request);
718719

719-
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
720-
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
721-
}
722-
else {
723-
prepareResponse(response);
724-
}
725-
726720
// Execute invokeHandlerMethod in synchronized block if required.
727721
if (this.synchronizeOnSession) {
728722
HttpSession session = request.getSession(false);
729723
if (session != null) {
730724
Object mutex = WebUtils.getSessionMutex(session);
731725
synchronized (mutex) {
732-
return invokeHandlerMethod(request, response, handlerMethod);
726+
mav = invokeHandlerMethod(request, response, handlerMethod);
733727
}
734728
}
735729
}
736730

737-
return invokeHandlerMethod(request, response, handlerMethod);
731+
mav = invokeHandlerMethod(request, response, handlerMethod);
732+
733+
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
734+
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
735+
}
736+
else {
737+
prepareResponse(response);
738+
}
739+
740+
return mav;
738741
}
739742

740743
/**

spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java

Lines changed: 34 additions & 29 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-2016 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.
@@ -20,6 +20,7 @@
2020
import java.util.HashSet;
2121
import java.util.Set;
2222
import java.util.concurrent.TimeUnit;
23+
2324
import javax.servlet.ServletException;
2425
import javax.servlet.http.HttpServletRequest;
2526
import javax.servlet.http.HttpServletResponse;
@@ -329,14 +330,16 @@ protected final void prepareResponse(HttpServletResponse response) {
329330
* @since 4.2
330331
*/
331332
protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) {
332-
String ccValue = cacheControl.getHeaderValue();
333-
if (ccValue != null) {
334-
// Set computed HTTP 1.1 Cache-Control header
335-
response.setHeader(HEADER_CACHE_CONTROL, ccValue);
336-
337-
if (response.containsHeader(HEADER_PRAGMA)) {
338-
// Reset HTTP 1.0 Pragma header if present
339-
response.setHeader(HEADER_PRAGMA, "");
333+
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
334+
String ccValue = cacheControl.getHeaderValue();
335+
if (ccValue != null) {
336+
// Set computed HTTP 1.1 Cache-Control header
337+
response.setHeader(HEADER_CACHE_CONTROL, ccValue);
338+
339+
if (response.containsHeader(HEADER_PRAGMA)) {
340+
// Reset HTTP 1.0 Pragma header if present
341+
response.setHeader(HEADER_PRAGMA, "");
342+
}
340343
}
341344
}
342345
}
@@ -352,30 +355,32 @@ protected final void applyCacheControl(HttpServletResponse response, CacheContro
352355
*/
353356
@SuppressWarnings("deprecation")
354357
protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) {
355-
if (this.useExpiresHeader || !this.useCacheControlHeader) {
356-
// Deprecated HTTP 1.0 cache behavior, as in previous Spring versions
357-
if (cacheSeconds > 0) {
358-
cacheForSeconds(response, cacheSeconds);
359-
}
360-
else if (cacheSeconds == 0) {
361-
preventCaching(response);
362-
}
363-
}
364-
else {
365-
CacheControl cControl;
366-
if (cacheSeconds > 0) {
367-
cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
368-
if (this.alwaysMustRevalidate) {
369-
cControl = cControl.mustRevalidate();
358+
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
359+
if (this.useExpiresHeader || !this.useCacheControlHeader) {
360+
// Deprecated HTTP 1.0 cache behavior, as in previous Spring versions
361+
if (cacheSeconds > 0) {
362+
cacheForSeconds(response, cacheSeconds);
363+
}
364+
else if (cacheSeconds == 0) {
365+
preventCaching(response);
370366
}
371-
}
372-
else if (cacheSeconds == 0) {
373-
cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
374367
}
375368
else {
376-
cControl = CacheControl.empty();
369+
CacheControl cControl;
370+
if (cacheSeconds > 0) {
371+
cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
372+
if (this.alwaysMustRevalidate) {
373+
cControl = cControl.mustRevalidate();
374+
}
375+
}
376+
else if (cacheSeconds == 0) {
377+
cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
378+
}
379+
else {
380+
cControl = CacheControl.empty();
381+
}
382+
applyCacheControl(response, cControl);
377383
}
378-
applyCacheControl(response, cControl);
379384
}
380385
}
381386

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

Lines changed: 2 additions & 2 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-2016 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,7 @@ public void parameterizableViewController() throws Exception {
4949
ParameterizableViewController pvc = new ParameterizableViewController();
5050
pvc.setViewName(viewName);
5151
// We don't care about the params.
52-
ModelAndView mv = pvc.handleRequest(new MockHttpServletRequest("GET", "foo.html"), null);
52+
ModelAndView mv = pvc.handleRequest(new MockHttpServletRequest("GET", "foo.html"), new MockHttpServletResponse());
5353
assertTrue("model has no data", mv.getModel().size() == 0);
5454
assertTrue("model has correct viewname", mv.getViewName().equals(viewName));
5555
assertTrue("getViewName matches", pvc.getViewName().equals(viewName));

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

Lines changed: 32 additions & 5 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-2016 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.
@@ -28,20 +28,23 @@
2828
import java.util.HashMap;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.concurrent.TimeUnit;
32+
3133
import javax.servlet.http.Cookie;
3234
import javax.servlet.http.HttpServletRequest;
3335
import javax.servlet.http.HttpServletResponse;
3436
import javax.validation.Valid;
3537

38+
import org.hamcrest.Matchers;
3639
import org.junit.After;
3740
import org.junit.Before;
3841
import org.junit.Test;
3942

4043
import org.springframework.beans.factory.annotation.Value;
4144
import org.springframework.beans.propertyeditors.CustomDateEditor;
4245
import org.springframework.core.MethodParameter;
46+
import org.springframework.http.CacheControl;
4347
import org.springframework.http.HttpEntity;
44-
import org.springframework.http.HttpHeaders;
4548
import org.springframework.http.HttpStatus;
4649
import org.springframework.http.ResponseEntity;
4750
import org.springframework.mock.web.test.MockHttpServletRequest;
@@ -85,6 +88,7 @@
8588
import static org.junit.Assert.assertNotNull;
8689
import static org.junit.Assert.assertNull;
8790
import static org.junit.Assert.assertSame;
91+
import static org.junit.Assert.assertThat;
8892
import static org.junit.Assert.assertTrue;
8993

9094
/**
@@ -262,6 +266,24 @@ public void handleHttpEntity() throws Exception {
262266
assertEquals(HttpStatus.ACCEPTED.value(), response.getStatus());
263267
assertEquals("Handled requestBody=[Hello Server]", new String(response.getContentAsByteArray(), "UTF-8"));
264268
assertEquals("headerValue", response.getHeader("header"));
269+
// set because of @SesstionAttributes
270+
assertEquals("no-store", response.getHeader("Cache-Control"));
271+
}
272+
273+
// SPR-13867
274+
@Test
275+
public void handleHttpEntityWithCacheControl() throws Exception {
276+
Class<?>[] parameterTypes = new Class<?>[] { HttpEntity.class };
277+
request.addHeader("Content-Type", "text/plain; charset=utf-8");
278+
request.setContent("Hello Server".getBytes("UTF-8"));
279+
280+
HandlerMethod handlerMethod = handlerMethod("handleHttpEntityWithCacheControl", parameterTypes);
281+
ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod);
282+
283+
assertNull(mav);
284+
assertEquals(HttpStatus.OK.value(), response.getStatus());
285+
assertEquals("Handled requestBody=[Hello Server]", new String(response.getContentAsByteArray(), "UTF-8"));
286+
assertThat(response.getHeaderValues("Cache-Control"), Matchers.contains("max-age=3600"));
265287
}
266288

267289
@Test
@@ -373,10 +395,15 @@ public String handleAndValidateRequestBody(@Valid TestBean modelAttr, Errors err
373395
}
374396

375397
public ResponseEntity<String> handleHttpEntity(HttpEntity<byte[]> httpEntity) throws Exception {
376-
HttpHeaders responseHeaders = new HttpHeaders();
377-
responseHeaders.set("header", "headerValue");
378398
String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]";
379-
return new ResponseEntity<String>(responseBody, responseHeaders, HttpStatus.ACCEPTED);
399+
return ResponseEntity.accepted()
400+
.header("header", "headerValue")
401+
.body(responseBody);
402+
}
403+
404+
public ResponseEntity<String> handleHttpEntityWithCacheControl(HttpEntity<byte[]> httpEntity) throws Exception {
405+
String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]";
406+
return ResponseEntity.ok().cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)).body(responseBody);
380407
}
381408

382409
public void handleRequestPart(@RequestPart String requestPart, Model model) {

0 commit comments

Comments
 (0)