The ConditionalContentCachingResponseWrapper returns the raw outputStream after 5.2.x. Once I try to write to outputStream directly, I am actually writing to the raw outputStream, which makes the raw response commited. Then the method isEligibleForEtag
will return false and ShallowEtagHeaderFilter won't work.
Before 5.1.x, the caching response will return caching outputStream and ShallowEtagHeaderFilter works well.
If it's a bug, please check MockHttpServletResponse. It won't be commited when you are trying to write to outputStream directly, which seems acting differently comparing with the implementation of Tomcat or Jetty.
Here is the example.
ApplicationLoader.java
@SpringBootApplication
@RestController
public class ApplicationLoader {
public static void main(String[] args) {
SpringApplication.run(ApplicationLoader.class, args);
}
@Bean
public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}
@GetMapping("/hello")
public void hello(@RequestParam(value = "name", defaultValue = "World") String name, HttpServletResponse httpServletResponse) throws IOException {
String msg = String.format("Hello %s!", name);
String etag = DigestUtils.md5DigestAsHex(name.getBytes(StandardCharsets.UTF_8));
httpServletResponse.setHeader(HttpHeaders.ETAG, etag);
OutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
return;
}
}
ETagTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ETagTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate = new TestRestTemplate();
@Test
public void helloWorld() throws Exception {
String url = "http://localhost:" + port + "/hello?name=World";
ResponseEntity<String> responseEntity = this.restTemplate.getForEntity(url, String.class);
assertEquals("Hello World!", responseEntity.getBody());
String etag = responseEntity.getHeaders().getETag();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.IF_NONE_MATCH, etag);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(null, httpHeaders);
responseEntity = restTemplate.exchange(url, HttpMethod.GET, request, String.class);
//The http status code will be 304 when using spring-boot-2.1.18.RELEASE, but 200 when using spring-boot-2.3.12.RELEASE
assertEquals(HttpStatus.NOT_MODIFIED, responseEntity.getStatusCode());
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.lmh</groupId>
<artifactId>spring-etag-bug</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>20</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--spring.boot.version>2.1.18.RELEASE</spring.boot.version-->
<spring.boot.version>2.3.12.RELEASE</spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Comment From: bclozel
Spring Boot 2.1.x and 2.3.x are not supported anymore. Can you try with a supported version and report back?
https://spring.io/projects/spring-boot#support
Comment From: LiuMenghan
Spring Boot 2.1.x and 2.3.x are not supported anymore. Can you try with a supported version and report back?
https://spring.io/projects/spring-boot#support
I tried with spring-boot-2.7.14 and it doesn't work just like spring-boot-2.3.12. I think Spring Boot. does not matter . It can be reproduced with 5.2.x or 5.3.x. But with 5.1.x, the test can be passed.
Comment From: bclozel
This is due to a bugfix in #24635.
Your controller implementation is wrong, as it manually sets the ETag header. You should either:
- manually set the ETag header, but then this means that you're taking full control over the response and it's your responsibility to deal with conditional requests (this is the behavior enforced in #24635).
- let the filter itself set the ETag value and everything works as expected
Changing your controller implementation to the following works:
@GetMapping("/hello")
public void hello(@RequestParam(value = "name", defaultValue = "World") String name, HttpServletResponse httpServletResponse) throws IOException {
String msg = String.format("Hello %s!", name);
OutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
Comment From: LiuMenghan
So should I return with http status code 304 by myself instead of using ShallowEtagHeaderFilter if I want to customize ETag header and write to OutputStream directly at the same time? I exptect ShallowEtagHeaderFilter should return with 304 and doesn't overwrite ETag header.
Comment From: bclozel
If you want to take full control over the conditional request process, you should. Setting the ETag header value yourself means that it's the case.
Comment From: LiuMenghan
There are some properties(such as timestamp) in my project which is different every request and must be exclued when calculating the ETag value, so I have to set ETag value in my controller. I don't want to take full control. I expect ShallowEtagHeaderFilter can help me return with 304 and not overwrite my ETag value.
Comment From: bclozel
If you are calculating the ETag yourself then you should not use the ShallowEtagFilter. You can consider returning a response entity like this: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-caching.html#mvc-caching-etag-lastmodified (this includes bodies like Resource if you need to manually write bytes).
For more questions please use StackOverflow and explain your use case. Thanks!
Comment From: LiuMenghan
Thank you.