spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.81k stars 38.18k forks source link

Content-Length value and actual length value can be different #32186

Closed chanhyeong closed 10 months ago

chanhyeong commented 10 months ago

Affects: 6.1.x

Condition

Bug

getContentLength() value and length of actual value can be different.


                    {
                        "contextContent": {
                            "aSource": {
                                "a": "20242024"
                            },
                            "bSource": {
                                "b": "20252025"
                            }
                        }
                    }
  1. I sent a body as json string like above, the byte array size of raw string is 369.
  2. And that number filled as Content-Length header in MockHttpServletRequestBuilder
  3. Content-Length header is sent as request header in apache httpcore RequestContent
    • HttpComponentsClientHttpRequest$BodyEntity#getContentLength
  4. But actual length value is 125, which is serialization result of jackson ObjectMapper (AbstractJackson2HttpMessageConverter)

This mismatch cause Connection Reset -> server wait until receiving 369 bytes, but client sent 125.

Before 6.1.x

This issue didn't exist because HttpComponentsClientHttpRequest use ByteArrayEntity that contains a serialization result of jackson ObjectMapper

https://github.com/spring-projects/spring-framework/blob/72835f10b9b07921978da419abf2a182abf67967/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java#L91

bclozel commented 10 months ago

Rather than providing your analysis can you share an example of application that shows the issue? It's hard for us to help you as we are missing the entire context.

We wouldn't need a complete example with the gateway but a simple http client request with invalid content length would be enough.

chanhyeong commented 10 months ago

@bclozel

I made a sample in https://github.com/chanhyeong/headersample You can reproduce this issue by following below.

  1. Run spring boot application
  2. Run failedBecauseOfInvalidContentLengthHeader() to see this issue
    • boot application server blames SocketTimeoutException
  3. Run successWithMatchingContentLength() to see success case when content length value matched
bclozel commented 10 months ago

@chanhyeong the project is not accessible.

chanhyeong commented 10 months ago

@bclozel

I'm sorry. I changed visibility as public.

bclozel commented 10 months ago

Thanks for the sample. I don't think this issue is related to Spring Framework.

Here, the request body is deserialized from JSON into a Map, then re-serialized as the request body to the proxied service. The "Content-Length" header is copied over directly. I don't think that this should happen. The "Content-Length" header should be overwritten, or the body should never be deserialized in the first place (this is quite inefficient in this case).

You can consider changing your controller to the following:

    @PostMapping("/**")
    public ResponseEntity<?> serverProxy(
        ProxyExchange<byte[]> proxyExchange,
        HttpServletRequest request,
        @RequestBody byte[] body
    ) {
        String url = assembleUrl(proxyExchange, request);
        return proxyExchange.uri(url).body(body).post();
    }

I think this has been raised already in spring-cloud/spring-cloud-gateway#3154 and I'll close this issue in favor of that one. There are other workarounds described there, and probably a future fix for this.

chanhyeong commented 10 months ago

Thanks. I doubted because HttpComponentsClientHttpRequest was changed.

Also, I'm going to add 'Content-Length' header into sensitive headers list temporarily to prevent propagating to downstream. It will be added by httpcore RequestContent.