I have (simplified) two unit test cases for one @RestController class:
1. A simple GET request test
2. A simple DELETE request test
When the tests are executed, the second test case always fails with this error:
I/O error on DELETE request for "http://localhost:8080/data/42": Unexpected end of file from server
org.springframework.web.client.ResourceAccessException: I/O error on DELETE request for "http://localhost:8080/data/42": Unexpected end of file from server
at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:915)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:895)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:790)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:672)
at org.springframework.boot.test.web.client.TestRestTemplate.exchange(TestRestTemplate.java:710)
at testproject.DataBoundaryTest.2-delete data(DataBoundaryTest.kt:29)
...
Caused by: java.net.SocketException: Unexpected end of file from server
at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:954)
at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:761)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1710)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1611)
at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529)
at org.springframework.http.client.SimpleClientHttpRequest.executeInternal(SimpleClientHttpRequest.java:88)
at org.springframework.http.client.AbstractStreamingClientHttpRequest.executeInternal(AbstractStreamingClientHttpRequest.java:70)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:889)
... 88 more
Code
- controller
@RestController
class DataBoundary() {
@GetMapping("/data")
fun getData(): List<String> {
return emptyList()
}
@DeleteMapping("/data/{id}")
fun deleteData(@PathVariable id: String): ResponseEntity<Any> {
return ResponseEntity.ok().build()
}
}
- test class:
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT )
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
@TestMethodOrder(MethodOrderer.MethodName::class)
class DataBoundaryTest(@Autowired private val restTemplate: TestRestTemplate) {
@Test
fun `1-get data`() {
restTemplate.getForObject<Any>("/data")
}
@Test
fun `2-delete data`() {
restTemplate.exchange("/data/42", HttpMethod.DELETE, HttpEntity.EMPTY, Unit::class.java)
}
}
Notes
The second test case does NOT fail if
* the HTTP method is changed from DELETE to PUT
* the order of the tests are changed (i.e. DELETE is tested before GET)
* the @DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD) is removed (but I need it because there is actually a database)
The second test case STILL fails if
* the HTTP method is changed from DELETE to POST
* Thread.sleep(5000) is added to the end of the first test case or at the beginning of the second test case
Version: 3.3.5 (dependencies implementation("org.springframework.boot:spring-boot-starter-web") and testImplementation("org.springframework.boot:spring-boot-starter-test")
The application itself works fine: I can manually execute a DELETE request after a GET request without problems.
Comment From: mhalbritter
Hello! Please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem. Preferably in Java. Thanks!
Comment From: abika
Here you go: https://github.com/abika/spring-bug-testproject
Run ./gradlew test to get the error.
Comment From: mhalbritter
Thanks for the sample!
This also fails with 3.2.11, and only if WebEnvironment.DEFINED_PORT is used. It works with WebEnvironment.RANDOM_PORT.
It also fails with this:
this.restTemplate = new RestTemplateBuilder().rootUri("http://localhost:" + this.port).build();
but not with this:
this.restTemplate = new RestTemplateBuilder().requestFactory(() -> new JdkClientHttpRequestFactory()).rootUri("http://localhost:" + this.port).build();
Comment From: nosan
I don't think it is a bug in Spring Boot, it looks like it is related to HttpURLConnection implementation. Probably because of that https://docs.oracle.com/javase/6/docs/technotes/guides/net/http-keepalive.html
Everything will be fine if you add the following to your test.
@BeforeAll
static void setUp() {
System.setProperty("http.keepAlive", "false");
}
Comment From: nosan
This also works fine
@Test
void a_get_data() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Connection", "close");
restTemplate.exchange("/data", HttpMethod.GET, new HttpEntity<>(httpHeaders), Void.class);
}
@Test
void b_delete_data() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Connection", "close");
restTemplate.exchange("/data/42", HttpMethod.PUT, new HttpEntity<>(httpHeaders), Void.class);
}
Comment From: philwebb
Thanks @nosan! I'm going to close this one since there's no action for us to take.
Comment From: nosan
@philwebb Just an additional evidence that is not Spring Boot bug:
package testproject;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.InetSocketAddress;
@TestMethodOrder(MethodOrderer.MethodName.class)
class DataBoundaryTest {
HttpServer httpServer;
RestTemplate restTemplate;
@BeforeEach
void setupServer() throws IOException {
// System.setProperty("http.keepAlive", "false");
restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new SimpleClientHttpRequestFactory());
httpServer = HttpServer.create();
httpServer.createContext("/", exchange -> {
exchange.sendResponseHeaders(200, 0);
exchange.close();
});
httpServer.bind(new InetSocketAddress(8080), 0);
httpServer.start();
}
@AfterEach
void tearDownServer() {
httpServer.stop(0);
}
@Test
void a_get_data() {
HttpHeaders httpHeaders = new HttpHeaders();
restTemplate.exchange("http://localhost:8080/data", HttpMethod.GET, new HttpEntity<>(httpHeaders), Void.class);
}
@Test
void b_delete_data() {
HttpHeaders httpHeaders = new HttpHeaders();
restTemplate.exchange("http://localhost:8080/data/42", HttpMethod.DELETE, new HttpEntity<>(httpHeaders),
Void.class);
}
}
I/O error on DELETE request for "http://localhost:8080/data/42": Connection reset org.springframework.web.client.ResourceAccessException: I/O error on DELETE request for > "http://localhost:8080/data/42": Connection reset