# Testing cohttp\_eio request handlers

**URL:** <https://discuss.ocaml.org/t/testing-cohttp-eio-request-handlers/18185>\
**Category:** Learning\
**Tags:** cohttp\
**Created:** [May 27, 2026, 9:04am UTC](https://discuss.ocaml.org/t/testing-cohttp-eio-request-handlers/18185 "2026-05-27T09:04:53Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![mneumann](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/mneumann/32/6522_2.png) [@mneumann](https://discuss.ocaml.org/u/mneumann)\
**Post date:** [May 27, 2026, 9:04am UTC](https://discuss.ocaml.org/t/testing-cohttp-eio-request-handlers/18185/1 "2026-05-27T09:04:53Z")

</div>

Hey,

I wrote a webservice using `cohttp_eio` and then wanted to test it, ideally without having to start a server listening on a network interface.

My rough idea was to craft the inputs for the request handler (HTTP verb, resource, headers, body) and then call the handler under test and finally inspect the output. Sounds easy… 🙂

FYI, a request handler in cohttp\_eio is just a function with three arguments `conn`, `request` and `body` which returns a `response`.

BUT: The `conn` and `response` caused me headaches as they are private types. While I was able to just drop the `conn` argument from my handlers as they are not used, the `response` was still there and is of type:

`type response = writer -> unit`

with an abstract `type writer`

I wasn’t able to figure out how to create a `writer`, so I discarded my initial idea and went with starting a `Cohttp_eio.Server` and using `Cohttp_eio.Client` to send real requests. Thanks to Eio’s deterministic “scheduling” I was able to do:

```auto
let stop, stop_signal = Eio.Promise.create () in
Eio.Fiber.both
      (fun () ->
        start_server ~stop ())
      (fun () ->
        test_server ();
        Eio.Promise.resolve stop_signal ())

```

No “ugly” polling required to wait until the server is up and running until I can start sending requests. Not sure if this approach is 100% bullet proof, but it seems to work; otherwise another Promise to signal “started” would work.

For testing request handlers, I think it would be nice to be able to “mock” a `Cohttp_eio` connection and writer. I am just not sure if it’s worth it.

Probably a better approach would be for me to completely hide the use of `Cohttp_eio` from withing my handler’s code and use my own representation of a response (and body). So instead of `Cohttp_eio.Server.respond_string` I’d have to return my own representation. And the `body` argument would become a simple function `fun () -> Eio.Flow.read_all body`.

I am happy to hear your view and how you would test request handlers (if you test them) 🙂

---

<div class="post-metadata">

**Author:** ![talex5](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/talex5/32/353_2.png) [@talex5](https://discuss.ocaml.org/u/talex5)\
**Post date:** [May 27, 2026, 6:26pm UTC](https://discuss.ocaml.org/t/testing-cohttp-eio-request-handlers/18185/2 "2026-05-27T18:26:16Z")

</div>

> [@mneumann](#):
>
> I discarded my initial idea and went with starting a `Cohttp_eio.Server` and using `Cohttp_eio.Client` to send real requests.

Seems reasonable to me. Might as well test it with the real cohttp logic!

> [@mneumann](#):
>
> No “ugly” polling required to wait until the server is up and running until I can start sending requests. Not sure if this approach is 100% bullet proof, but it seems to work; otherwise another Promise to signal “started” would work.

`Server.run` takes a `listening_socket`. So I’d create that first (outside of the `Fiber.both`). Then it doesn’t matter if the server yields while starting up.

---

<div class="post-metadata">

**Author:** ![mneumann](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/mneumann/32/6522_2.png) [@mneumann](https://discuss.ocaml.org/u/mneumann)\
**Post date:** [May 28, 2026, 4:10pm UTC](https://discuss.ocaml.org/t/testing-cohttp-eio-request-handlers/18185/3 "2026-05-28T16:10:03Z")

</div>

Thanks! I initially created the `listening_socket` within the first Fiber and it seemed to work fine, but now moved it out, just to be safe.

I run into a github ci problem with that code, but that turned out to be unrelated. When run from a github action you need to use “127.0.0.1” instead of “localhost” in the HTTP client.
