Skip to content

Commit 953608e

Browse files
malkuschbclozel
authored andcommitted
Improve ETag & Last-Modifed support in WebRequest
This change improves the following use cases with `WebRequest.checkNotModified(String etag)` and `WebRequest.checkNotModified(long lastModifiedTimeStamp)`: 1) Allow weak comparisons for ETags Per rfc7232 section-2.3, ETags can be strong or weak; this change allows comparing weak forms `W/"etagvalue"` but does not make a difference between strong and weak comparisons. 2) Allow multiple ETags in client requests HTTP clients can send multiple ETags values in a single header such as: `If-None-Match: "firstvalue", "secondvalue"` This change makes sure each value is compared to the one provided by the application side. 3) Extended support for ETag values This change adds padding `"` to the ETag value provided by the application, if not already done: `etagvalue` => `"etagvalue"` It also supports wildcard values `*` that can be sent by HTTP clients. 4) Sending validation headers for 304 responses As defined in https://tools.ietf.org/html/rfc7232#section-4.1 `304 Not Modified` reponses must generate `Etag` and `Last-Modified` HTTP headers, as they would have for a `200 OK` response. 5) Providing a new method to validate both Etag & Last-Modified Also, this change adds a new method `WebRequest.checkNotModified(String etag, long lastModifiedTimeStamp)` in order to support validation of both `If-None-Match` and `Last-Modified` headers sent by HTTP clients, if both values are supported by the application code. Even though this approach is recommended by the HTTP rfc (setting both Etag and Last-Modified headers in the response), this requires more application logic and may not apply to all resources produced by the application. Issue: SPR-11324
1 parent e086a63 commit 953608e

File tree

6 files changed

+418
-158
lines changed

6 files changed

+418
-158
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ public boolean checkNotModified(String eTag) {
155155
return false;
156156
}
157157

158+
/**
159+
* Last-modified handling not supported for portlet requests:
160+
* As a consequence, this method always returns {@code false}.
161+
*
162+
* @since 4.2
163+
*/
164+
@Override
165+
public boolean checkNotModified(String etag, long lastModifiedTimestamp) {
166+
return false;
167+
}
168+
158169
@Override
159170
public String getDescription(boolean includeClientInfo) {
160171
ExternalContext externalContext = getExternalContext();

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

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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;
@@ -35,6 +36,9 @@
3536
* {@link WebRequest} adapter for an {@link javax.servlet.http.HttpServletRequest}.
3637
*
3738
* @author Juergen Hoeller
39+
* @author Brian Clozel
40+
* @author Markus Malkusch
41+
*
3842
* @since 2.0
3943
*/
4044
public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
@@ -72,7 +76,6 @@ public ServletWebRequest(HttpServletRequest request, HttpServletResponse respons
7276
super(request, response);
7377
}
7478

75-
7679
@Override
7780
public Object getNativeRequest() {
7881
return getRequest();
@@ -93,7 +96,6 @@ public <T> T getNativeResponse(Class<T> requiredType) {
9396
return WebUtils.getNativeResponse(getResponse(), requiredType);
9497
}
9598

96-
9799
/**
98100
* Return the HTTP method of the request.
99101
* @since 4.0.2
@@ -169,66 +171,113 @@ public boolean isSecure() {
169171
}
170172

171173
@Override
172-
@SuppressWarnings("deprecation")
173174
public boolean checkNotModified(long lastModifiedTimestamp) {
174175
HttpServletResponse response = getResponse();
175-
if (lastModifiedTimestamp >= 0 && !this.notModified &&
176-
(response == null || !response.containsHeader(HEADER_LAST_MODIFIED))) {
177-
long ifModifiedSince = -1;
178-
try {
179-
ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE);
180-
}
181-
catch (IllegalArgumentException ex) {
182-
String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE);
183-
// Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
184-
int separatorIndex = headerValue.indexOf(';');
185-
if (separatorIndex != -1) {
186-
String datePart = headerValue.substring(0, separatorIndex);
187-
try {
188-
ifModifiedSince = Date.parse(datePart);
189-
}
190-
catch (IllegalArgumentException ex2) {
191-
// Giving up
176+
if (lastModifiedTimestamp >= 0 && !this.notModified) {
177+
if (response == null || !response.containsHeader(HEADER_LAST_MODIFIED)) {
178+
this.notModified = isTimeStampNotModified(lastModifiedTimestamp);
179+
if (response != null) {
180+
if (this.notModified && supportsNotModifiedStatus()) {
181+
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
192182
}
183+
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
193184
}
194185
}
195-
this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
196-
if (response != null) {
197-
if (this.notModified && supportsNotModifiedStatus()) {
198-
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
186+
}
187+
return this.notModified;
188+
}
189+
190+
@SuppressWarnings("deprecation")
191+
private boolean isTimeStampNotModified(long lastModifiedTimestamp) {
192+
long ifModifiedSince = -1;
193+
try {
194+
ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE);
195+
}
196+
catch (IllegalArgumentException ex) {
197+
String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE);
198+
// Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
199+
int separatorIndex = headerValue.indexOf(';');
200+
if (separatorIndex != -1) {
201+
String datePart = headerValue.substring(0, separatorIndex);
202+
try {
203+
ifModifiedSince = Date.parse(datePart);
199204
}
200-
else {
201-
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
205+
catch (IllegalArgumentException ex2) {
206+
// Giving up
202207
}
203208
}
204209
}
205-
return this.notModified;
210+
return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
206211
}
207212

208213
@Override
209214
public boolean checkNotModified(String etag) {
210215
HttpServletResponse response = getResponse();
211-
if (StringUtils.hasLength(etag) && !this.notModified &&
212-
(response == null || !response.containsHeader(HEADER_ETAG))) {
213-
String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH);
214-
this.notModified = etag.equals(ifNoneMatch);
215-
if (response != null) {
216-
if (this.notModified && supportsNotModifiedStatus()) {
217-
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
218-
}
219-
else {
216+
if (StringUtils.hasLength(etag) && !this.notModified) {
217+
if (response == null || !response.containsHeader(HEADER_ETAG)) {
218+
etag = addEtagPadding(etag);
219+
this.notModified = isETagNotModified(etag);
220+
if (response != null) {
221+
if (this.notModified && supportsNotModifiedStatus()) {
222+
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
223+
}
220224
response.setHeader(HEADER_ETAG, etag);
221225
}
222226
}
223227
}
224228
return this.notModified;
225229
}
226230

231+
private String addEtagPadding(String etag) {
232+
if (!(etag.startsWith("\"") || etag.startsWith("W/\"")) || !etag.endsWith("\"")) {
233+
etag = "\"" + etag + "\"";
234+
}
235+
return etag;
236+
}
237+
238+
private boolean isETagNotModified(String etag) {
239+
if (StringUtils.hasLength(etag)) {
240+
String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH);
241+
if (StringUtils.hasLength(ifNoneMatch)) {
242+
String[] clientETags = StringUtils.delimitedListToStringArray(ifNoneMatch, ",", " ");
243+
for (String clientETag : clientETags) {
244+
// compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
245+
if (StringUtils.hasLength(clientETag) &&
246+
(clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", ""))
247+
|| clientETag.equals("*"))) {
248+
return true;
249+
}
250+
}
251+
}
252+
}
253+
return false;
254+
}
255+
227256
private boolean supportsNotModifiedStatus() {
228257
String method = getRequest().getMethod();
229258
return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method));
230259
}
231260

261+
@Override
262+
public boolean checkNotModified(String etag, long lastModifiedTimestamp) {
263+
HttpServletResponse response = getResponse();
264+
if (StringUtils.hasLength(etag) && !this.notModified) {
265+
if (response == null ||
266+
(!response.containsHeader(HEADER_ETAG) && !response.containsHeader(HEADER_LAST_MODIFIED))) {
267+
etag = addEtagPadding(etag);
268+
this.notModified = isETagNotModified(etag) && isTimeStampNotModified(lastModifiedTimestamp);
269+
if (response != null) {
270+
if (this.notModified && supportsNotModifiedStatus()) {
271+
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
272+
}
273+
response.setHeader(HEADER_ETAG, etag);
274+
response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
275+
}
276+
}
277+
}
278+
return this.notModified;
279+
}
280+
232281
public boolean isNotModified() {
233282
return this.notModified;
234283
}

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -141,9 +141,12 @@ public interface WebRequest extends RequestAttributes {
141141
* model.addAttribute(...);
142142
* return "myViewName";
143143
* }</pre>
144-
* <p><strong>Note:</strong> that you typically want to use either
144+
* <p><strong>Note:</strong> you can use either
145145
* this {@code #checkNotModified(long)} method; or
146-
* {@link #checkNotModified(String)}, but not both.
146+
* {@link #checkNotModified(String)}. If you want enforce both
147+
* a strong entity tag and a Last-Modified value,
148+
* as recommended by the HTTP specification,
149+
* then you should use {@link #checkNotModified(String, long)}.
147150
* <p>If the "If-Modified-Since" header is set but cannot be parsed
148151
* to a date value, this method will ignore the header and proceed
149152
* with setting the last-modified timestamp on the response.
@@ -172,9 +175,12 @@ public interface WebRequest extends RequestAttributes {
172175
* model.addAttribute(...);
173176
* return "myViewName";
174177
* }</pre>
175-
* <p><strong>Note:</strong> that you typically want to use either
178+
* <p><strong>Note:</strong> you can use either
176179
* this {@code #checkNotModified(String)} method; or
177-
* {@link #checkNotModified(long)}, but not both.
180+
* {@link #checkNotModified(long)}. If you want enforce both
181+
* a strong entity tag and a Last-Modified value,
182+
* as recommended by the HTTP specification,
183+
* then you should use {@link #checkNotModified(String, long)}.
178184
* @param etag the entity tag that the application determined
179185
* for the underlying resource. This parameter will be padded
180186
* with quotes (") if necessary.
@@ -184,6 +190,42 @@ public interface WebRequest extends RequestAttributes {
184190
*/
185191
boolean checkNotModified(String etag);
186192

193+
/**
194+
* Check whether the request qualifies as not modified given the
195+
* supplied {@code ETag} (entity tag) and last-modified timestamp,
196+
* as determined by the application.
197+
* <p>This will also transparently set the appropriate response headers,
198+
* for both the modified case and the not-modified case.
199+
* <p>Typical usage:
200+
* <pre class="code">
201+
* public String myHandleMethod(WebRequest webRequest, Model model) {
202+
* String eTag = // application-specific calculation
203+
* long lastModified = // application-specific calculation
204+
* if (request.checkNotModified(eTag, lastModified)) {
205+
* // shortcut exit - no further processing necessary
206+
* return null;
207+
* }
208+
* // further request processing, actually building content
209+
* model.addAttribute(...);
210+
* return "myViewName";
211+
* }</pre>
212+
* <p><strong>Note:</strong> The HTTP specification recommends
213+
* setting both ETag and Last-Modified values, but you can also
214+
* use {@code #checkNotModified(String)} or
215+
* {@link #checkNotModified(long)}.
216+
* @param etag the entity tag that the application determined
217+
* for the underlying resource. This parameter will be padded
218+
* with quotes (") if necessary.
219+
* @param lastModifiedTimestamp the last-modified timestamp that
220+
* the application determined for the underlying resource
221+
* @return whether the request qualifies as not modified,
222+
* allowing to abort request processing and relying on the response
223+
* telling the client that the content has not been modified
224+
*
225+
* @since 4.2
226+
*/
227+
boolean checkNotModified(String etag, long lastModifiedTimestamp);
228+
187229
/**
188230
* Get a short description of this request,
189231
* typically containing request URI and session id.

0 commit comments

Comments
 (0)