Hello, I try to use the new RestClient but seems to encounter a strange behavior.

This is just a simple post query but it gave me HTTP/1.1 header parser received no bytes with RestClient and success with RestTemplate.

A simplified code is :

RestTemplate restTemplate = new RestTemplate();
RestClient restClient = RestClient.builder().baseUrl("").build();

String format = "{\"schema\":\"{\\\"type\\\":\\\"record\\\",\\\"name\\\":\\\"Value\\\",\\\"fields\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\"}]}\"}";

HttpHeaders headers = new HttpHeaders();
headers.setContentType(APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(format, headers);
String statusCode = restTemplate.postForObject("http://localhost:8081/subjects/local.dkt-member.streaming.public.fact.sample.v1.avro-value/versions", request, String.class);

LOG.debug("post {}", statusCode);

statusCode = restClient
        .method(HttpMethod.POST)
        .uri("http://localhost:8081/subjects/local.dkt-member.streaming.public.fact.sample.v1.avro-value/versions")
        .contentType(APPLICATION_JSON)
        .body(format)
        .retrieve()
        .body(String.class);

LOG.debug("post {}", statusCode);

when I made a curl this work and here it is the response:

curl -v -H "Content-Type: application/json" -X POST http://localhost:8081/subjects/foo-value/versions -d '{"schema":"{\"type\":\"record\",\"name\":\"Value\",\"fields\":[{\"name\":\"key\",\"type\":\"string\"}]}"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:8081 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8081...
* connect to ::1 port 8081 from ::1 port 51534 failed: Connection refused
*   Trying 127.0.0.1:8081...
* Connected to localhost (127.0.0.1) port 8081
> POST /subjects/foo-value/versions HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/8.6.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 105
> 
< HTTP/1.1 200 OK
< Content-Type: application/vnd.schemaregistry.v1+json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: DELETE, GET, OPTIONS, POST, PUT
< Access-Control-Allow-Headers: Authorization, Content-Type
< Server: Karapace/3.7.1
< access-control-expose-headers: etag
< etag: "d2ce28b9a7fd7e4407e2b0fd499b7fe4"
< Content-Length: 8
< Date: Thu, 25 Jul 2024 19:51:49 GMT
< 
* Connection #0 to host localhost left intact
{"id":1}

There is a minimal reproducer project

debug-rest-client.tar.gz

To run reproducer start kafka and karapace compose stack with docker compose -f src/main/resources/docker-compose.yaml up --renew-anon-volumes --force-recreate kafka1 schema-registry (sorry for fat stack but it's the only server causing this)

ps : on server side it give a strange stack with method UNKNOWN

schema-registry  | aiohttp.access       MainThread      INFO            0.000412s - "POST /subjects/foo-value/versions HTTP/1.1" 400 "Java-http-client/21" response=186b request_body=106b
schema-registry  | aiohttp.server       MainThread      ERROR           Error handling request
schema-registry  | Traceback (most recent call last):
schema-registry  |   File "/venv/lib/python3.10/site-packages/aiohttp/web_protocol.py", line 332, in data_received
schema-registry  |     messages, upgraded, tail = self._request_parser.feed_data(data)
schema-registry  |   File "aiohttp/_http_parser.pyx", line 551, in aiohttp._http_parser.HttpParser.feed_data
schema-registry  | aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'Invalid method encountered'"
schema-registry  | aiohttp.access       MainThread      INFO            0.002235s - "UNKNOWN / HTTP/1.0" 400 "-" response=205b request_body=-b

I try to debug it but don't find anything.

Maybe there is an obvious difference between my both call that I didn't see ?

Comment From: bclozel

What happens if you create your RestClient using:

RestClient restClient = RestClient.create(restTemplate);

Are the clients still behaving differently?

Comment From: RouxAntoine

I do

        RestTemplate restTemplate = new RestTemplate();
        RestClient restClient = RestClient.create(restTemplate);

        String schemaEscaped = "{\"type\":\"record\",\"name\":\"Value\",\"fields\":[{\"name\":\"key\",\"type\":\"string\"}]}"
                .replaceAll("\"", "\\\\\"")
                .replaceAll("\n", "")
                .replaceAll(" ", "");

        LOG.debug("schemaEscaped {}", schemaEscaped);

        String format = String.format("{\"schema\":\"%s\"}", schemaEscaped);
        LOG.debug("format {}", format);

And yes RestClient behave similarly as RestTemplate


also notice

instead of

String format = "{\"schema\":\"{\\\"type\\\":\\\"record\\\",\\\"name\\\":\\\"Value\\\",\\\"fields\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\"}]}\"}";

if I use

String schemaEscaped = "{\"type\":\"record\",\"name\":\"Value\",\"fields\":[{\"name\":\"key\",\"type\":\"string\"}]}"
        .replaceAll("\"", "\\\\\"")
        .replaceAll("\n", "")
        .replaceAll(" ", "");

String format = String.format("{\"schema\":\"%s\"}", schemaEscaped);

the error became 400 Bad Request: "Missing request JSON body"

Comment From: RouxAntoine

Oh no

the error just change

from

400 Bad Request: "Missing request JSON body"
HTTP/1.1 header parser received no bytes

depending of run not related to the format input string

Comment From: bclozel

There is a difference between the default RestTemplate and RestClient instances: the default client HTTP library being used underneath. With RestTemplate, we're using the SimpleClientHttpRequestFactory (so java.net.HttpURLConnection effectively). With RestClient, we're using the JdkClientHttpRequestFactory (so JDK 11 java.net.http.HttpClient).

When I run the application with the -Djdk.httpclient.HttpClient.log=errors,requests,headers VM option, I'm seeing in the logs:

Jul 26, 2024 9:53:47 AM jdk.internal.net.http.Http1Request headers
INFO: REQUEST: http://localhost:8081/subjects/foo-value/versions POST
Jul 26, 2024 9:53:47 AM jdk.internal.net.http.Http1Request logHeaders
INFO: HEADERS: REQUEST HEADERS:
POST /subjects/foo-value/versions HTTP/1.1
Connection: Upgrade, HTTP2-Settings
Content-Length: 106
Host: localhost:8081
HTTP2-Settings: AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA
Upgrade: h2c
User-Agent: Java-http-client/21.0.3
Content-Type: application/json

Jul 26, 2024 9:53:47 AM jdk.internal.net.http.Http1Response lambda$readHeadersAsync$0
INFO: HEADERS: RESPONSE HEADERS:
    content-length: 25
    content-type: text/plain; charset=utf-8
    date: Fri, 26 Jul 2024 07:53:47 GMT
    server: Python/3.10 aiohttp/3.8.4

So the JDK client tries to upgrade to HTTP/2 with h2c proactively and it seems that the python HTTP parser chokes on that.

Configuring the HTTP client to not do that makes things work as well:

HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
JdkClientHttpRequestFactory jdkClientHttpRequestFactory = new JdkClientHttpRequestFactory(httpClient);
RestClient restClient = RestClient.builder().requestFactory(jdkClientHttpRequestFactory).baseUrl("").build();

In summary, this issue comes from a combination of an HTTP parser bug (because the HTTP request looks valid to me) and a HttpClient default behavior in Java. I'm closing this issue as a result, since Spring Framework behaves correctly here.

For your application, you can use a different request factory or use the code snippet that I shared above.

Comment From: RouxAntoine

Hello, Thanks a lot for explanation, the parsing bug is a little worrying. But yes spring seems indeed not concerned. Have a greet days.