Skip to content

Directly encoding a ujson object silently activates chunked upload #204

@fhackett

Description

@fhackett

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions