-
-
Notifications
You must be signed in to change notification settings - Fork 89
Description
When directly sending a ujson object (relying on the geny based encoding feature), the requests library defaults to adding what I think are HTTP/2 streaming markers. This is fine for an endpoint that supports HTTP/2, because the standard allows the client to switch to the HTTP/2 encoding without checking that the server actually supports it. When communicating with a genuine HTTP/1.1 only server, this causes a parsing error when receiving the payload, because the additional data is not valid JSON (which the endpoint would expect to parse, not knowing its actual significance).
Here is a simple example:
//> using dependency "com.lihaoyi::requests:0.9.0"
//> using dependency "com.lihaoyi::upickle:4.2.1"
requests.post(
"http://localhost:3000/",
data = ujson.Obj(),
)Most endpoints actually support HTTP/2 and this will often just work, so we need to look at the actual request using something like npx http-echo-server. Using that utility, we see this:
[server] event: connection (socket#2)
[socket#2] event: resume
[socket#2] event: data
--> POST / HTTP/1.1
--> Connection: Upgrade, HTTP2-Settings
--> Host: localhost:3000
--> HTTP2-Settings: AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA
--> Transfer-encoding: chunked
--> Upgrade: h2c
--> Accept: */*
--> Accept-Encoding: gzip, deflate
--> Content-Type: application/json
--> User-Agent: requests-scala
-->
-->
[socket#2] event: data
--> 2
--> {}
-->
[socket#2] event: data
--> 0
-->
-->
[socket#2] event: prefinish
[socket#2] event: finish
[socket#2] event: readable
[socket#2] event: end
[socket#2] event: close
If the server is HTTP/2 compliant, this is a supported behavior and it will work (the app will see {}). However, for an HTTP/1.1 endpoint, the application layer will really see something like 2\n{}\n\n0, rather than {}.
To show that this is specifically how the JSON gets encoded, and not some more fundamental bug in the underlying Java library, I can add .toString, manually specify the content type, and the request changes significantly.
//> using dependency "com.lihaoyi::requests:0.9.0"
//> using dependency "com.lihaoyi::upickle:4.2.1"
requests.post(
"http://localhost:3000/",
headers = Map(
"Content-Type" -> "application/json",
),
data = ujson.Obj().toString,
)Now, we can still see it try to negotiate HTTP/2, but the extra stream markers are gone, and a pure HTTP/1.1 server will accept the request, ignore the negotiation it doesn't understand, and all is fine.
[socket#3] event: resume
[socket#3] event: data
--> POST / HTTP/1.1
--> Connection: Upgrade, HTTP2-Settings
--> Content-Length: 2
--> Host: localhost:3000
--> HTTP2-Settings: AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA
--> Upgrade: h2c
--> Accept: */*
--> Accept-Encoding: gzip, deflate
--> Content-Type: application/json
--> User-Agent: requests-scala
-->
-->
[socket#3] event: data
--> {}
[socket#3] event: prefinish
[socket#3] event: finish
[socket#3] event: readable
[socket#3] event: end
[socket#3] event: close
This is quite a confusing issue, which manifests in practice as an oddity like "why does literally the same request work with curl and not scala-requests?". It would be nice to see a fix, but at least I hope this issue documents the problem and my workaround: just use .toString, which is fine for my simple use case sending a few bytes of JSON to trigger an event.