Skip to content

Commit c53c8bf

Browse files
committed
Set 304 status on ServerResponse when ETag/LastModified match
This commit checks the Etag/LastModified headers on the incoming request, and sets a 304 Not Modified status with no body when they match, by delegating to ServerWebExchange.checkNotModified. Issue: SPR-16348
1 parent c211e39 commit c53c8bf

File tree

6 files changed

+208
-31
lines changed

6 files changed

+208
-31
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,8 @@ public T entity() {
224224
}
225225

226226
@Override
227-
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
228-
ServerHttpResponse response = exchange.getResponse();
229-
writeStatusAndHeaders(response);
230-
return inserter().insert(response, new BodyInserter.Context() {
227+
protected Mono<Void> writeToInternal(ServerWebExchange exchange, Context context) {
228+
return inserter().insert(exchange.getResponse(), new BodyInserter.Context() {
231229
@Override
232230
public List<HttpMessageWriter<?>> messageWriters() {
233231
return context.messageWriters();

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import org.springframework.http.HttpStatus;
3636
import org.springframework.http.MediaType;
3737
import org.springframework.http.ResponseCookie;
38-
import org.springframework.http.server.reactive.ServerHttpResponse;
3938
import org.springframework.lang.Nullable;
4039
import org.springframework.util.Assert;
4140
import org.springframework.util.LinkedMultiValueMap;
@@ -184,9 +183,7 @@ public Map<String, Object> model() {
184183
}
185184

186185
@Override
187-
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
188-
ServerHttpResponse response = exchange.getResponse();
189-
writeStatusAndHeaders(response);
186+
protected Mono<Void> writeToInternal(ServerWebExchange exchange, Context context) {
190187
MediaType responseContentType = exchange.getResponse().getHeaders().getContentType();
191188
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
192189
Stream<ViewResolver> viewResolverStream = context.viewResolvers().stream();

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.web.reactive.function.server;
1818

1919
import java.net.URI;
20+
import java.time.Instant;
2021
import java.time.ZonedDateTime;
2122
import java.util.Arrays;
23+
import java.util.EnumSet;
2224
import java.util.HashMap;
2325
import java.util.LinkedHashSet;
2426
import java.util.List;
@@ -278,6 +280,8 @@ public Mono<ServerResponse> render(String name, Map<String, ?> model) {
278280

279281
static abstract class AbstractServerResponse implements ServerResponse {
280282

283+
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
284+
281285
final int statusCode;
282286

283287
private final HttpHeaders headers;
@@ -319,7 +323,21 @@ public MultiValueMap<String, ResponseCookie> cookies() {
319323
return this.cookies;
320324
}
321325

322-
protected void writeStatusAndHeaders(ServerHttpResponse response) {
326+
@Override
327+
public final Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
328+
writeStatusAndHeaders(exchange.getResponse());
329+
330+
Instant lastModified = Instant.ofEpochMilli(headers().getLastModified());
331+
HttpMethod httpMethod = exchange.getRequest().getMethod();
332+
if (SAFE_METHODS.contains(httpMethod) && exchange.checkNotModified(headers().getETag(), lastModified)) {
333+
return exchange.getResponse().setComplete();
334+
}
335+
else {
336+
return writeToInternal(exchange, context);
337+
}
338+
}
339+
340+
private void writeStatusAndHeaders(ServerHttpResponse response) {
323341
if (response instanceof AbstractServerHttpResponse) {
324342
((AbstractServerHttpResponse) response).setStatusCodeValue(this.statusCode);
325343
}
@@ -335,6 +353,8 @@ protected void writeStatusAndHeaders(ServerHttpResponse response) {
335353
copy(this.cookies, response.getCookies());
336354
}
337355

356+
protected abstract Mono<Void> writeToInternal(ServerWebExchange exchange, Context context);
357+
338358
private static <K,V> void copy(MultiValueMap<K,V> src, MultiValueMap<K,V> dst) {
339359
if (!src.isEmpty()) {
340360
src.entrySet().stream()
@@ -358,8 +378,7 @@ public WriterFunctionServerResponse(int statusCode, HttpHeaders headers,
358378
}
359379

360380
@Override
361-
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
362-
writeStatusAndHeaders(exchange.getResponse());
381+
protected Mono<Void> writeToInternal(ServerWebExchange exchange, Context context) {
363382
return this.writeFunction.apply(exchange, context);
364383
}
365384
}
@@ -381,10 +400,8 @@ public BodyInserterServerResponse(int statusCode, HttpHeaders headers,
381400
}
382401

383402
@Override
384-
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
385-
ServerHttpResponse response = exchange.getResponse();
386-
writeStatusAndHeaders(response);
387-
return this.inserter.insert(response, new BodyInserter.Context() {
403+
protected Mono<Void> writeToInternal(ServerWebExchange exchange, Context context) {
404+
return this.inserter.insert(exchange.getResponse(), new BodyInserter.Context() {
388405
@Override
389406
public List<HttpMessageWriter<?>> messageWriters() {
390407
return context.messageWriters();

spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -18,6 +18,8 @@
1818

1919
import java.nio.ByteBuffer;
2020
import java.time.ZonedDateTime;
21+
import java.time.format.DateTimeFormatter;
22+
import java.time.temporal.ChronoUnit;
2123
import java.util.Collections;
2224
import java.util.EnumSet;
2325
import java.util.List;
@@ -44,6 +46,7 @@
4446
import org.springframework.http.codec.HttpMessageWriter;
4547
import org.springframework.http.server.reactive.ServerHttpResponse;
4648
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
49+
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
4750
import org.springframework.mock.web.test.server.MockServerWebExchange;
4851
import org.springframework.util.LinkedMultiValueMap;
4952
import org.springframework.util.MultiValueMap;
@@ -232,4 +235,52 @@ public List<ViewResolver> viewResolvers() {
232235
assertNotNull(exchange.getResponse().getBody());
233236
}
234237

238+
@Test
239+
public void notModifiedEtag() {
240+
String etag = "\"foo\"";
241+
EntityResponse<String> responseMono = EntityResponse.fromObject("bar")
242+
.eTag(etag)
243+
.build()
244+
.block();
245+
246+
MockServerHttpRequest request = MockServerHttpRequest.get("http://example.com")
247+
.header(HttpHeaders.IF_NONE_MATCH, etag)
248+
.build();
249+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
250+
251+
responseMono.writeTo(exchange, DefaultServerResponseBuilderTests.EMPTY_CONTEXT);
252+
253+
MockServerHttpResponse response = exchange.getResponse();
254+
assertEquals(HttpStatus.NOT_MODIFIED, response.getStatusCode());
255+
StepVerifier.create(response.getBody())
256+
.expectError(IllegalStateException.class)
257+
.verify();
258+
}
259+
260+
261+
@Test
262+
public void notModifiedLastModified() {
263+
ZonedDateTime now = ZonedDateTime.now();
264+
ZonedDateTime oneMinuteBeforeNow = now.minus(1, ChronoUnit.MINUTES);
265+
266+
EntityResponse<String> responseMono = EntityResponse.fromObject("bar")
267+
.lastModified(oneMinuteBeforeNow)
268+
.build()
269+
.block();
270+
271+
MockServerHttpRequest request = MockServerHttpRequest.get("http://example.com")
272+
.header(HttpHeaders.IF_MODIFIED_SINCE,
273+
DateTimeFormatter.RFC_1123_DATE_TIME.format(now))
274+
.build();
275+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
276+
277+
responseMono.writeTo(exchange, DefaultServerResponseBuilderTests.EMPTY_CONTEXT);
278+
279+
MockServerHttpResponse response = exchange.getResponse();
280+
assertEquals(HttpStatus.NOT_MODIFIED, response.getStatusCode());
281+
StepVerifier.create(response.getBody())
282+
.expectError(IllegalStateException.class)
283+
.verify();
284+
}
285+
235286
}

spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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,9 @@
1616

1717
package org.springframework.web.reactive.function.server;
1818

19+
import java.time.ZonedDateTime;
20+
import java.time.format.DateTimeFormatter;
21+
import java.time.temporal.ChronoUnit;
1922
import java.util.ArrayList;
2023
import java.util.Collections;
2124
import java.util.List;
@@ -28,9 +31,11 @@
2831
import reactor.test.StepVerifier;
2932

3033
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpStatus;
3135
import org.springframework.http.MediaType;
3236
import org.springframework.http.ResponseCookie;
3337
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
38+
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
3439
import org.springframework.mock.web.test.server.MockServerWebExchange;
3540
import org.springframework.util.LinkedMultiValueMap;
3641
import org.springframework.util.MultiValueMap;
@@ -40,7 +45,7 @@
4045
import org.springframework.web.reactive.result.view.ViewResolverSupport;
4146
import org.springframework.web.server.ServerWebExchange;
4247

43-
import static org.junit.Assert.assertEquals;
48+
import static org.junit.Assert.*;
4449
import static org.mockito.Mockito.*;
4550

4651
/**
@@ -181,4 +186,52 @@ protected Mono<Void> renderInternal(Map<String, Object> renderAttributes,
181186

182187
}
183188

189+
@Test
190+
public void notModifiedEtag() {
191+
String etag = "\"foo\"";
192+
RenderingResponse responseMono = RenderingResponse.create("bar")
193+
.header(HttpHeaders.ETAG, etag)
194+
.build()
195+
.block();
196+
197+
MockServerHttpRequest request = MockServerHttpRequest.get("http://example.com")
198+
.header(HttpHeaders.IF_NONE_MATCH, etag)
199+
.build();
200+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
201+
202+
responseMono.writeTo(exchange, DefaultServerResponseBuilderTests.EMPTY_CONTEXT);
203+
204+
MockServerHttpResponse response = exchange.getResponse();
205+
assertEquals(HttpStatus.NOT_MODIFIED, response.getStatusCode());
206+
StepVerifier.create(response.getBody())
207+
.expectError(IllegalStateException.class)
208+
.verify();
209+
}
210+
211+
@Test
212+
public void notModifiedLastModified() {
213+
ZonedDateTime now = ZonedDateTime.now();
214+
ZonedDateTime oneMinuteBeforeNow = now.minus(1, ChronoUnit.MINUTES);
215+
216+
RenderingResponse responseMono = RenderingResponse.create("bar")
217+
.header(HttpHeaders.LAST_MODIFIED, DateTimeFormatter.RFC_1123_DATE_TIME.format(oneMinuteBeforeNow))
218+
.build()
219+
.block();
220+
221+
MockServerHttpRequest request = MockServerHttpRequest.get("http://example.com")
222+
.header(HttpHeaders.IF_MODIFIED_SINCE,
223+
DateTimeFormatter.RFC_1123_DATE_TIME.format(now))
224+
.build();
225+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
226+
227+
responseMono.writeTo(exchange, DefaultServerResponseBuilderTests.EMPTY_CONTEXT);
228+
229+
MockServerHttpResponse response = exchange.getResponse();
230+
assertEquals(HttpStatus.NOT_MODIFIED, response.getStatusCode());
231+
StepVerifier.create(response.getBody())
232+
.expectError(IllegalStateException.class)
233+
.verify();
234+
}
235+
236+
184237
}

0 commit comments

Comments
 (0)