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