Cohttp and 100 continue?

I have a small server that exposes both an HTTP/1.1 and HTTP/2 gRPC endpoint. I was surprised to see that the HTTP/1.1 endpoint almost always takes ~1s to complete a request that the gRPC endpoint would complete almost instantly.

Turns out this is because with HTTP/1.1 Curl is sending an ‘Expect: 100 Continue’ header and waits until a timeout (1s) before sending the request body. See here for details Expect 100-continue - Everything curl. This can be worked around in the client (by not sending the Expect: 100 header), but would be much better to fix it in the server where the problem is.

AFAICT Cohttp doesn’t implement this header, and I don’t even see how I’d be able to send some headers, and then send the real response later, at least not with the ‘Server.make’ API. Perhaps by wrapping ‘Server.make_expert’ to send the 100-continue reply as appropriate and then write the real reply and body later again?

I hit exactly this issue a few weeks ago when doing a service integration using Ezcurl. It turns out that, in general, HTTP servers simply don’t support the Expect header (and Dream is no different in not providing a way to become conformant).

Some relevant links:

The problem with 100 continue is that it somehow breaks the simple request/response cycle for your application.

I think last time I thought about the problem I came to the conclusion that best is for your low level http library to unconditionally respond without surfacing the interaction higher up.

Totally defeats the purpose of it and has the potential of making obscure debugging sessions (“hey the library is sometimes responding without telling me there was a request”) but it avoids complexifying your API and the latency for the rare clients that use this header.

1 Like

Thanks, I’ll need to take another look at the client, apparently curl should only send Expect: 100-continue if the contents is >1MiB (not the case for me), or content size is unknown (likely the case for me).

I thought trying something like this: whenever the request handler starts to read the Cohttp Body (and this is a post with expect:100) it should send the 100-continue header if not sent already. That would be fairly transparent if the handler is a bit careful about ordering, e.g. first checking request method, headers and Uri path, and responding with an error if they are not right: if the request is wrong the server would reply with an error code without the client having to send a (potentially large) body. And if the request handler starts reading the body then we need to send the 100-continue otherwise it might be 1s (or more) until we get any data to read.
This should be achievable by concating a ‘send-100-continue’ side-effecting stream at the head of the body, although I’ll have to dig deeper because I also need to flush that so the client can see it (and setting the flush flag on Request didn’t seem enough, but perhaps I did something wrong).

Ezcurl-0.2.2 uses chunked encoding for POST. The master version of ezcurl already avoids this problem for small requests by setting the proper size (with CURLOPT_POSTFIELDSIZE) when the size is known, and removing the expect header entirely when not known.

Still it’d be good to fix this server-side too, looking into it…
Even if I write a small callback that always does Server.respond ~status:Continue Curl gets stuck for the 1s waiting for it. Looking at ‘server.ml’ it looks like there is an Lwt.finalize call that does Body.drain_body. Thus Cohttp will only send back anything to the client after the request body has been fully read. So the only place that 100-continue handling can be implemented is inside Cohttp itself, there doesn’t appear to be anything that an application can do to send 100 continue back correctly.

Fix for Cohttp-lwt here: Cohttp-lwt: Send 100-continue on POST with Expect: 100-continue by edwintorok · Pull Request #979 · mirage/ocaml-cohttp · GitHub.
It’d be nice to also fix this for Cohttp-async and Httpaf, however I don’t use those currently. Some unit tests would be nice too, I’ll try to find some time to write one for cohttp-lwt later.

1 Like