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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions