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
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.