I will start by saying this is an extremely weird issue involving file uploads > 65KB and TestRestTemplate. And since TestRestTemplate and RestTemplate both reply on the Apache HTTP client it's possible this isn't even a Spring issue.

I have been debugging this for 4 days it's not clear to me why this is happening and I could use some help.

Reproduction

Git repo containing reproduction: https://github.com/workmanw/spring-test-bug-demo

Steps

  1. Run mvn clean test one or more times on Mac OS (I have not been able to reproduce in docker or on linux)
  2. You will see the following error:
2021-07-02 21:26:49.921  INFO 25652 --- [o-auto-1-exec-1] o.apache.coyote.http11.Http11Processor   : Error parsing HTTP request header
 Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.

java.lang.IllegalArgumentException: Invalid character found in method name [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0x0d0x0a--6hXCZ7NXvctrRhMdQfCslrHNTVm930eH--0x0d0x0aPOST]. HTTP method names must be tokens
    at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:417) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:261) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.39.jar:9.0.39]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]

Explanation

What appears to be happening is that when files over 65K are uploaded this way the contents of the body in the buffer gets shifted around when it's sent over the wire.

The request body should look like this:

POST /v1/demo/upload HTTP/1.1
Content-Type: multipart/form-data;charset=UTF-8;boundary=jgV0Bpae6rNX-ZcB2FrJmBOVr8kS3yzAvq
Content-Length: 66203
Host: localhost:61694
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.12 (Java/11.0.11)
Accept-Encoding: gzip,deflate

--jgV0Bpae6rNX-ZcB2FrJmBOVr8kS3yzAvq
Content-Disposition: form-data; name="file"; filename="SingleFile.txt"
Content-Type: text/plain
Content-Length: 66000

AAAAAAA...<66,000x A>...AAAAAAA
--jgV0Bpae6rNX-ZcB2FrJmBOVr8kS3yzAvq--

However with this reproduction, the last portion of the body gets shifted to the beginning of the request. Looks like this:

AAAAAAAAAA
--jgV0Bpae6rNX-ZcB2FrJmBOVr8kS3yzAvq--
POST /v1/demo/upload HTTP/1.1
Content-Type: multipart/form-data;charset=UTF-8;boundary=jgV0Bpae6rNX-ZcB2FrJmBOVr8kS3yzAvq
Content-Length: 66203
Host: localhost:61694
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.12 (Java/11.0.11)
Accept-Encoding: gzip,deflate

--jgV0Bpae6rNX-ZcB2FrJmBOVr8kS3yzAvq
Content-Disposition: form-data; name="file"; filename="SingleFile.txt"
Content-Type: text/plain
Content-Length: 66000

AAAAAAAAAAA...<66,000x A>...

The above results in a malformed HTTP request that Tomcat is unable to parse. This results in the Invalid character found in method name error observed above.

Observation

If a breakpoint is added to Http11InputBuffer::parseRequestLine, which is on the Tomcat receiving end, this can be observed by watching contents of the byteBuffer as its read from the socket.

SpringBoot RestTemplate writes out of order data to socket on Mac OS when request body > 65KB

If a breakpoint is added to SocketOutputStream::socketWrite, which is on the test sending end, the contents can be observed as serialized correctly.

Writing header: SpringBoot RestTemplate writes out of order data to socket on Mac OS when request body > 65KB

Writing body: SpringBoot RestTemplate writes out of order data to socket on Mac OS when request body > 65KB

Comment From: bclozel

If the content is correct at the SocketOutputStream::socketWrite level, then it's probably not a Spring Boot issue.

Also, not being able to reproduce this issue on other hosts is another clue. I'm wondering if this might be an issue with a local proxy or antivirus. This wouldn't be a first unfortunately. Can you temporarily disable such tools and still reproduce the issue?

Comment From: workmanw

@bclozel It is reproducible by everyone on my team. I tried on our family computer this morning w/ the same JVM and could not reproduce it of course 😡 . Let me see if my corporate overloads will allow me to disable the some of the bloatware and retest. Can we leave this open until next week. I will follow up either way.

One other interesting data point seems to be if I remove the @AutoConfigureWireMock(port = 0) annotation it resolves the issue.

Comment From: bclozel

I think you're onto something. I've consistently reproduced this issue on my macOS laptop (of course bloatware involved here as well).

But I cannot reproduce the problem after removing the spring-cloud-starter-contract-stub-runner dependency. There might be a problem with the instrumentation provided by spring-cloud-contract or a library conflict? I'm not familiar enough with that project to find the cause of this issue.

Could you create an issue on the dedicated issue tracker here: https://github.com/spring-cloud/spring-cloud-contract/issues

I'm closing this issue for now as I can't reproduce it without Spring Cloud being involved. Your sample application is well written so I'm sure this is good reproducer for Spring Cloud Contract.

Note that I've locally simplified the test method and I can still reproduce the problem:

    private void testUploadFile(int fileSize) {
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        TextFileResource textFileResource = new TextFileResource("SingleFile.txt", fileSize);
        body.add("file", textFileResource);
        ResponseEntity<Map> s = testRestTemplate.postForEntity(URI.create("/v1/demo/upload"), body, Map.class);
        Map<String, String> responseBody = s.getBody();
        assertEquals(Integer.toString(fileSize), responseBody.get("uploadedFileSize"));
    }

Comment From: workmanw

@bclozel Thanks for the simplified reproduction. I also noticed the spring-cloud-contract issue when trying to create the issue. I attributed it to a timing or race condition because without it it uses a much simpler code path.

We use cloudstrike as our antivirus software. Any chance you're using the same? I have some early indications that that might be the culprit.

Comment From: bclozel

@workmanw I'm not using the same antivirus software.

Comment From: workmanw

@bclozel We got to the root of the issue working with our corporate IT. It turns out that Apple made in 11.13.1 that broke the way a CrowdStrike module interacted with the Apple API. Specially CrowdStrike's Falcon EDR module. I apologize for not having any sources to cite, the only references I was given to the issue were behind private ticket systems :( . We disabled the CrowdStrike EDR module and could no longer reproduce the issue.

Thanks a ton for pointing me toward antivirus.