Skip to content

Commit 0d6f800

Browse files
committed
Support conditional updates in ServletWebRequest
Prior to this commit, `ServletWebRequest.checkNotModified` would only support conditional GET/HEAD requests with "If-Modified-Since" and/or "If-None-Match" request headers. In those cases, the server would return "HTTP 304 Not Modified" responses if the resource didn't change. This commit adds support for conditional update requests, such as POST/PUT/DELETE requests with "If-Unmodified-Since" request headers. If the underlying resource has been modified since the specified date, the server will return a "409 Precondition failed" response status to prevent concurrent updates. Even if the modification status of the resource is reversed here (modified vs. not modified), we're keeping here the same intent for the return value, which signals if the response requires more processing or if the handler method can return immediately: ``` if (request.checkNotModified(lastModified)) { // shortcut exit - no further processing necessary return null; } ``` Issue: SPR-13863
1 parent b3abd3b commit 0d6f800

File tree

4 files changed

+115
-41
lines changed

4 files changed

+115
-41
lines changed

spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java

Lines changed: 62 additions & 19 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.
@@ -21,6 +21,7 @@
2121
import java.util.Iterator;
2222
import java.util.Locale;
2323
import java.util.Map;
24+
2425
import javax.servlet.http.HttpServletRequest;
2526
import javax.servlet.http.HttpServletResponse;
2627
import javax.servlet.http.HttpSession;
@@ -47,6 +48,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
4748

4849
private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
4950

51+
private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
52+
5053
private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
5154

5255
private static final String HEADER_LAST_MODIFIED = "Last-Modified";
@@ -55,6 +58,12 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
5558

5659
private static final String METHOD_HEAD = "HEAD";
5760

61+
private static final String METHOD_POST = "POST";
62+
63+
private static final String METHOD_PUT = "PUT";
64+
65+
private static final String METHOD_DELETE = "DELETE";
66+
5867

5968
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
6069
private static final boolean servlet3Present =
@@ -183,11 +192,18 @@ public boolean checkNotModified(long lastModifiedTimestamp) {
183192
if (isCompatibleWithConditionalRequests(response)) {
184193
this.notModified = isTimestampNotModified(lastModifiedTimestamp);
185194
if (response != null) {
186-
if (this.notModified && supportsNotModifiedStatus()) {
187-
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
195+
if (supportsNotModifiedStatus()) {
196+
if (this.notModified) {
197+
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
198+
}
199+
if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
200+
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
201+
}
188202
}
189-
if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
190-
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
203+
else if (supportsConditionalUpdate()) {
204+
if (this.notModified) {
205+
response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
206+
}
191207
}
192208
}
193209
}
@@ -223,14 +239,21 @@ public boolean checkNotModified(String etag, long lastModifiedTimestamp) {
223239
etag = addEtagPadding(etag);
224240
this.notModified = isEtagNotModified(etag) && isTimestampNotModified(lastModifiedTimestamp);
225241
if (response != null) {
226-
if (this.notModified && supportsNotModifiedStatus()) {
227-
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
228-
}
229-
if (isHeaderAbsent(response, HEADER_ETAG)) {
230-
response.setHeader(HEADER_ETAG, etag);
242+
if (supportsNotModifiedStatus()) {
243+
if (this.notModified) {
244+
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
245+
}
246+
if (isHeaderAbsent(response, HEADER_ETAG)) {
247+
response.setHeader(HEADER_ETAG, etag);
248+
}
249+
if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
250+
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
251+
}
231252
}
232-
if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
233-
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
253+
else if (supportsConditionalUpdate()) {
254+
if (this.notModified) {
255+
response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
256+
}
234257
}
235258
}
236259
}
@@ -250,7 +273,8 @@ private boolean isCompatibleWithConditionalRequests(HttpServletResponse response
250273
return true;
251274
}
252275
return HttpStatus.valueOf(response.getStatus()).is2xxSuccessful();
253-
} catch (IllegalArgumentException e) {
276+
}
277+
catch (IllegalArgumentException e) {
254278
return true;
255279
}
256280
}
@@ -268,27 +292,46 @@ private boolean supportsNotModifiedStatus() {
268292
return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method));
269293
}
270294

271-
@SuppressWarnings("deprecation")
295+
private boolean supportsConditionalUpdate() {
296+
String method = getRequest().getMethod();
297+
String ifUnmodifiedHeader = getRequest().getHeader(HEADER_IF_UNMODIFIED_SINCE);
298+
return (METHOD_POST.equals(method) || METHOD_PUT.equals(method) || METHOD_DELETE.equals(method))
299+
&& StringUtils.hasLength(ifUnmodifiedHeader);
300+
}
301+
272302
private boolean isTimestampNotModified(long lastModifiedTimestamp) {
273-
long ifModifiedSince = -1;
303+
long ifModifiedSince = parseDateHeader(HEADER_IF_MODIFIED_SINCE);
304+
if (ifModifiedSince != -1) {
305+
return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
306+
}
307+
long ifUnmodifiedSince = parseDateHeader(HEADER_IF_UNMODIFIED_SINCE);
308+
if (ifUnmodifiedSince != -1) {
309+
return (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
310+
}
311+
return false;
312+
}
313+
314+
@SuppressWarnings("deprecation")
315+
private long parseDateHeader(String headerName) {
316+
long dateValue = -1;
274317
try {
275-
ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE);
318+
dateValue = getRequest().getDateHeader(headerName);
276319
}
277320
catch (IllegalArgumentException ex) {
278-
String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE);
321+
String headerValue = getRequest().getHeader(headerName);
279322
// Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
280323
int separatorIndex = headerValue.indexOf(';');
281324
if (separatorIndex != -1) {
282325
String datePart = headerValue.substring(0, separatorIndex);
283326
try {
284-
ifModifiedSince = Date.parse(datePart);
327+
dateValue = Date.parse(datePart);
285328
}
286329
catch (IllegalArgumentException ex2) {
287330
// Giving up
288331
}
289332
}
290333
}
291-
return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
334+
return dateValue;
292335
}
293336

294337
private boolean isEtagNotModified(String etag) {

spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java

Lines changed: 15 additions & 15 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.
@@ -126,10 +126,10 @@ public interface WebRequest extends RequestAttributes {
126126
boolean isSecure();
127127

128128
/**
129-
* Check whether the request qualifies as not modified given the
129+
* Check whether the requested resource has been modified given the
130130
* supplied last-modified timestamp (as determined by the application).
131-
* <p>This will also transparently set the appropriate response headers,
132-
* for both the modified case and the not-modified case.
131+
* <p>This will also transparently set the "Last-Modified" response header
132+
* and HTTP status when applicable.
133133
* <p>Typical usage:
134134
* <pre class="code">
135135
* public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -142,6 +142,8 @@ public interface WebRequest extends RequestAttributes {
142142
* model.addAttribute(...);
143143
* return "myViewName";
144144
* }</pre>
145+
* <p>This method works with conditional GET/HEAD requests, but
146+
* also with conditional POST/PUT/DELETE requests.
145147
* <p><strong>Note:</strong> you can use either
146148
* this {@code #checkNotModified(long)} method; or
147149
* {@link #checkNotModified(String)}. If you want enforce both
@@ -160,10 +162,10 @@ public interface WebRequest extends RequestAttributes {
160162
boolean checkNotModified(long lastModifiedTimestamp);
161163

162164
/**
163-
* Check whether the request qualifies as not modified given the
165+
* Check whether the requested resource has been modified given the
164166
* supplied {@code ETag} (entity tag), as determined by the application.
165-
* <p>This will also transparently set the appropriate response headers,
166-
* for both the modified case and the not-modified case.
167+
* <p>This will also transparently set the "ETag" response header
168+
* and HTTP status when applicable.
167169
* <p>Typical usage:
168170
* <pre class="code">
169171
* public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -185,18 +187,16 @@ public interface WebRequest extends RequestAttributes {
185187
* @param etag the entity tag that the application determined
186188
* for the underlying resource. This parameter will be padded
187189
* with quotes (") if necessary.
188-
* @return whether the request qualifies as not modified,
189-
* allowing to abort request processing and relying on the response
190-
* telling the client that the content has not been modified
190+
* @return true if the request does not require further processing.
191191
*/
192192
boolean checkNotModified(String etag);
193193

194194
/**
195-
* Check whether the request qualifies as not modified given the
195+
* Check whether the requested resource has been modified given the
196196
* supplied {@code ETag} (entity tag) and last-modified timestamp,
197197
* as determined by the application.
198198
* <p>This will also transparently set the "ETag" and "Last-Modified"
199-
* response headers, for both the modified case and the not-modified case.
199+
* response headers, and HTTP status when applicable.
200200
* <p>Typical usage:
201201
* <pre class="code">
202202
* public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -210,6 +210,8 @@ public interface WebRequest extends RequestAttributes {
210210
* model.addAttribute(...);
211211
* return "myViewName";
212212
* }</pre>
213+
* <p>This method works with conditional GET/HEAD requests, but
214+
* also with conditional POST/PUT/DELETE requests.
213215
* <p><strong>Note:</strong> The HTTP specification recommends
214216
* setting both ETag and Last-Modified values, but you can also
215217
* use {@code #checkNotModified(String)} or
@@ -219,9 +221,7 @@ public interface WebRequest extends RequestAttributes {
219221
* with quotes (") if necessary.
220222
* @param lastModifiedTimestamp the last-modified timestamp that
221223
* the application determined for the underlying resource
222-
* @return whether the request qualifies as not modified,
223-
* allowing to abort request processing and relying on the response
224-
* telling the client that the content has not been modified
224+
* @return true if the request does not require further processing.
225225
* @since 4.2
226226
*/
227227
boolean checkNotModified(String etag, long lastModifiedTimestamp);

spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java

Lines changed: 27 additions & 3 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.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.web.context.request;
1818

19+
import static org.junit.Assert.*;
20+
1921
import java.text.SimpleDateFormat;
2022
import java.util.Arrays;
2123
import java.util.Date;
@@ -32,8 +34,6 @@
3234
import org.springframework.mock.web.test.MockHttpServletRequest;
3335
import org.springframework.mock.web.test.MockHttpServletResponse;
3436

35-
import static org.junit.Assert.*;
36-
3737
/**
3838
* Parameterized tests for ServletWebRequest
3939
* @author Juergen Hoeller
@@ -293,4 +293,28 @@ public void checkModifiedTimestampWithLengthPart() throws Exception {
293293
assertEquals(dateFormat.format(epochTime), servletResponse.getHeader("Last-Modified"));
294294
}
295295

296+
@Test
297+
public void checkNotModifiedTimestampConditionalPut() throws Exception {
298+
long currentEpoch = currentDate.getTime();
299+
long oneMinuteAgo = currentEpoch - (1000 * 60);
300+
servletRequest.setMethod("PUT");
301+
servletRequest.addHeader("If-UnModified-Since", currentEpoch);
302+
303+
assertFalse(request.checkNotModified(oneMinuteAgo));
304+
assertEquals(200, servletResponse.getStatus());
305+
assertEquals(null, servletResponse.getHeader("Last-Modified"));
306+
}
307+
308+
@Test
309+
public void checkNotModifiedTimestampConditionalPutConflict() throws Exception {
310+
long currentEpoch = currentDate.getTime();
311+
long oneMinuteAgo = currentEpoch - (1000 * 60);
312+
servletRequest.setMethod("PUT");
313+
servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo);
314+
315+
assertTrue(request.checkNotModified(currentEpoch));
316+
assertEquals(412, servletResponse.getStatus());
317+
assertEquals(null, servletResponse.getHeader("Last-Modified"));
318+
}
319+
296320
}

src/asciidoc/web-mvc.adoc

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4449,17 +4449,24 @@ This can be achieved as follows:
44494449
----
44504450

44514451
There are two key elements here: calling `request.checkNotModified(lastModified)` and
4452-
returning `null`. The former sets the response status to 304 before it returns `true`.
4452+
returning `null`. The former sets the appropriate response status and headers
4453+
before it returns `true`.
44534454
The latter, in combination with the former, causes Spring MVC to do no further
44544455
processing of the request.
44554456

44564457
Note that there are 3 variants for this:
44574458

44584459
* `request.checkNotModified(lastModified)` compares lastModified with the
4459-
`'If-Modified-Since'` request header
4460-
* `request.checkNotModified(eTag)` compares eTag with the `'ETag'` request header
4460+
`'If-Modified-Since'` or `'If-Unmodified-Since'` request header
4461+
* `request.checkNotModified(eTag)` compares eTag with the `'If-None-Match'` request header
44614462
* `request.checkNotModified(eTag, lastModified)` does both, meaning that both
4462-
conditions should be valid for the server to issue an `HTTP 304 Not Modified` response
4463+
conditions should be valid
4464+
4465+
When receiving conditional `'GET'`/`'HEAD'` requests, `checkNotModified` will check
4466+
that the resource has not been modified and if so, it will result in a `HTTP 304 Not Modified`
4467+
response. In case of conditional `'POST'`/`'PUT'`/`'DELETE'` requests, `checkNotModified`
4468+
will check that the resource has not been modified and if it has been, it will result in a
4469+
`HTTP 409 Precondition Failed` response to prevent concurrent modifications.
44634470

44644471

44654472
[[mvc-httpcaching-shallowetag]]

0 commit comments

Comments
 (0)