{"id":13726295,"url":"https://github.com/mirage/ocaml-cohttp","last_synced_at":"2025-10-05T14:21:48.916Z","repository":{"id":678526,"uuid":"322217","full_name":"mirage/ocaml-cohttp","owner":"mirage","description":"An OCaml library for HTTP clients and servers using Lwt or Async","archived":false,"fork":false,"pushed_at":"2025-03-17T05:13:25.000Z","size":7969,"stargazers_count":730,"open_issues_count":96,"forks_count":177,"subscribers_count":30,"default_branch":"main","last_synced_at":"2025-04-06T14:07:18.128Z","etag":null,"topics":["http","http-client","lwt","ocaml","unix"],"latest_commit_sha":null,"homepage":"","language":"OCaml","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mirage.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2009-09-30T12:48:23.000Z","updated_at":"2025-04-01T09:38:06.000Z","dependencies_parsed_at":"2023-10-27T11:23:02.660Z","dependency_job_id":"c8e46dea-cc02-4cdb-ab08-33af3f12e035","html_url":"https://github.com/mirage/ocaml-cohttp","commit_stats":{"total_commits":2480,"total_committers":100,"mean_commits":24.8,"dds":0.6411290322580645,"last_synced_commit":"716d6e66be35ed11967f3984ace27c1fa327b8ed"},"previous_names":[],"tags_count":87,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mirage%2Focaml-cohttp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mirage%2Focaml-cohttp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mirage%2Focaml-cohttp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mirage%2Focaml-cohttp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mirage","download_url":"https://codeload.github.com/mirage/ocaml-cohttp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248750008,"owners_count":21155682,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","http-client","lwt","ocaml","unix"],"created_at":"2024-08-03T01:02:58.379Z","updated_at":"2025-10-05T14:21:43.881Z","avatar_url":"https://github.com/mirage.png","language":"OCaml","readme":"## ocaml-cohttp -- an OCaml library for HTTP clients and servers [![Main workflow](https://github.com/mirage/ocaml-cohttp/actions/workflows/workflow.yml/badge.svg)](https://github.com/mirage/ocaml-cohttp/actions/workflows/workflow.yml)\n\nCohttp is an OCaml library for creating HTTP daemons. It has a portable\nHTTP parser, and implementations using various asynchronous programming\nlibraries:\n\n* `Http` provides essential type definitions used in Cohttp and an extremely\n  fast http parser. It is designed to have no dependencies and make it easy\n  for other packages to easily interoperate with Cohttp.\n* `Cohttp_lwt_unix` uses the [Lwt](https://ocsigen.org/lwt/) library, and\n  specifically the UNIX bindings. It uses [ocaml-tls](https://github.com/mirleft/ocaml-tls)\n  as the TLS implementation to handle HTTPS connections.\n* `Cohttp_async` uses the [Async](https://realworldocaml.org/v1/en/html/concurrent-programming-with-async.html)\n  library and `async_ssl` to handle HTTPS connections.\n* `Cohttp_lwt` exposes an OS-independent Lwt interface, which is used\n  by the [Mirage](https://mirage.io/) interface to generate standalone\n  microkernels (use the cohttp-mirage subpackage).\n* `Cohttp_lwt_jsoo` compiles to a JavaScript module that maps the Cohttp\n  calls to XMLHTTPRequests.  This is used to compile OCaml libraries like\n  the GitHub bindings to JavaScript and still run efficiently.\n* `Cohttp_curl` uses `libcurl`, via `ocurl`, as backend. It also comes\n  with lwt (`Cohttp_curl_lwt`) and async backends (`Cohttp_curl_async`).\n* `Cohttp_eio` uses `eio` to leverage new features from multicore ocaml 5.0.\n* `Cohttp_server_lwt_unix` uses lwt to implement a more efficient web server\n  with a minimal interface.\n\nYou can implement other targets using the parser very easily. Look at the `IO`\nsignature in `lib/s.mli` and implement that in the desired backend.\n\nYou can find help from cohttp users and maintainers at the\n[discuss.ocaml.org](https://discuss.ocaml.org) forum or on the\n[OCaml discord server](https://discord.gg/cCYQbqN).\n\n## Table of contents\n\n- [Installation](#installation)\n- [Client Tutorial](#client-tutorial)\n  * [Compile and execute with dune](#compile-and-execute-with-dune)\n- [Dealing with timeouts](#dealing-with-timeouts)\n- [Managing sessions](#managing-sessions)\n- [Multipart form data](#multipart-form-data)\n- [Creating custom resolver: a Docker Socket Client example](#creating-custom-resolver--a-docker-socket-client-example)\n- [Dealing with redirects](#dealing-with-redirects)\n- [Basic Server Tutorial](#basic-server-tutorial)\n  * [Compile and execute with dune](#compile-and-execute-with-dune-1)\n- [Installed Binaries](#installed-binaries)\n- [Debugging](#debugging)\n- [Important Links](#important-links)\n\n\n## Installation\n\nLatest stable version should be obtained from `opam`. Make sure to install the\nspecific backends you want as well. E.g.\n\n```\n$ opam install cohttp-lwt-unix cohttp-async\n```\n\nYou can also obtain the development release:\n\n```\n$ opam pin add cohttp --dev-repo\n```\n\n## Client Tutorial\n\nCohttp provides clients for Async, Lwt, and Js_of_ocaml (Lwt based). In this tutorial,\nwe will use the lwt client but the example should be easily translatable to Async.\n\nTo create a simple request, use one of the methods in `Cohttp_lwt_unix.Client`.\n`call` is the most general, there are also http method specialized such as\n`get`, `post`, etc.\n\nFor example downloading the reddit frontpage:\n\n```ocaml\nopen Lwt\nopen Cohttp\nopen Cohttp_lwt_unix\n\nlet main =\n  Client.get (Uri.of_string \"https://www.reddit.com/\") \u003e\u003e= fun (resp, body) -\u003e\n  let code = resp |\u003e Response.status |\u003e Code.code_of_status in\n  Printf.printf \"Response code: %d\\n\" code;\n  Printf.printf \"Headers: %s\\n\" (resp |\u003e Response.headers |\u003e Header.to_string);\n  body |\u003e Cohttp_lwt.Body.to_string \u003e|= fun body -\u003e\n  Printf.printf \"Body of length: %d\\n\" (String.length body);\n  body\n\nlet () =\n  let body = Lwt_main.run main in\n  print_endline (\"Received body\\n\" ^ body)\n```\n\nThere are a few things to notice:\n\n* We open 2 modules. `Cohttp` contains the backend independent modules and\n  `Cohttp_lwt_unix` the lwt + unix specific ones.\n* `Client.get` accepts a `Uri.t` and makes an http request. `Client.get` also\n  accepts optional arguments for things like header information.\n* The http response is returned in a tuple. The first element of the tuple\n  contains the response's status code, headers, http version, etc. The second\n  element contains the body.\n* The body is then converted to a string and is returned (after the length is\n  printed). Note that `Cohttp_lwt.Body.to_string` hence it's up to us to keep\n  a reference to the result.\n* We must trigger lwt's event loop for the request to run. `Lwt_main.run` will\n  run the event loop and return with final value of `body` which we then print.\n\nNote that `Cohttp_lwt_unix`/`Cohttp_async` are able to request an HTTPS page\nby default. For `Cohttp_lwt_unix` users can use [ocaml-tls](https://github.com/mirleft/ocaml-tls.git) by installing `tls-lwt` or [ocaml-ssl](https://github.com/savonet/ocaml-ssl) by installing `lwt_ssl`. The latter is the default if both are installed but it is possible to force the selection of tls with the environment variable `CONDUIT_TLS=native`. For `Cohttp_async` the default is to use\n`async_ssl` (but users are able to use `ocaml-tls` with some modifications).\n\nConsult the following modules for reference:\n\n* [Cohttp_lwt.Client](https://github.com/mirage/ocaml-cohttp/blob/master/cohttp-lwt/src/s.ml)\n* [Cohttp_async.Client](https://github.com/mirage/ocaml-cohttp/blob/master/cohttp-async/src/client.mli)\n\nThe full documentation for the latest published version of the library is\navailable on the [repository github pages](https://mirage.github.io/ocaml-cohttp/).\n\n### Compile and execute with dune\n\nCreate this `dune` file\n```\ncat - \u003e dune \u003c\u003cEOF\n(executable\n (public_name client_example)\n (name client_example)\n (libraries cohttp-lwt-unix))\nEOF\n```\nthen build and execute the example with\n```\n$ dune exec ./client_example.exe\n```\n\n## Dealing with timeouts\n\nYou can use [`Lwt.pick`](https://ocsigen.org/lwt/4.1.0/api/Lwt) to set a timeout\non the execution of a thread. For example, say that you want to set a timeout on\nthe `Client.get` thread in the example above, then you could modify the get call\nas follows\n\n```ocaml\nlet compute ~time ~f =\n  Lwt.pick\n    [\n      (f () \u003e|= fun v -\u003e `Done v)\n    ; (Lwt_unix.sleep time \u003e|= fun () -\u003e `Timeout)\n    ]\n\nlet body =\n  let get () = Client.get (Uri.of_string \"https://www.reddit.com/\") in\n  compute ~time:0.1 ~f:get \u003e\u003e= function\n  | `Timeout -\u003e failwith \"Timeout expired\"\n  | `Done (resp, body) -\u003e Lwt.return (resp, body)\n```\n\nExecuting the code, which you can actually try by calling\n```\n$ dune exec examples/lwt_unix_doc/client_lwt_timeout.exe\n```\nthe call will most likely fail with the following output\n```\nFatal error: exception (Failure \"Timeout expired\")\n```\n\nSimilarly, in the case of `cohttp-async` you can directly use Async's\n[`with_timeout`](https://ocaml.janestreet.com/ocaml-core/latest/doc/async_unix/Async_unix/Clock/index.html#val-with_timeout) function.\nFor example,\n\n```ocaml\nlet get_body ~uri ~timeout =\n    let%bind _, body = Cohttp_async.Client.get ~interrupt:(after (sec timeout)) uri in\n    Body.to_string body\n\nlet body =\n  let uri = Uri.of_string \"https://www.reddit.com/\" in\n  let timeout = 0.1 in\n  Clock.with_timeout (sec timeout) (get_body ~uri ~timeout)\n  \u003e\u003e| function\n  | `Result body -\u003e Log.debug logger \"body: %s\" body\n  | `Timeout  -\u003e Log.debug logger \"Timeout with url:%s\" url\n```\n\n## Managing sessions\n\nManaging sessions and saving cookies across requests is not directly supported by\n`cohttp`. It is not hard to roll out a custom solution, but an alternative is\nto use the [`session`](https://github.com/inhabitedtype/ocaml-session) library,\nwhich is compatible with `cohttp`.\n\n## Multipart form data\n\nMultipart form data is not supported out of the box but is provided by external libraries:\n- [`multipart_form`](https://github.com/dinosaure/multipart_form) which has bounded memory consumption even when transferring large amount of data\n- [`multipart-form-data`](https://github.com/cryptosense/multipart-form-data)\n- [`http-multipart-formdata`](https://github.com/lemaetech/http-multipart-formdata) which however does not support streaming\n\n## Creating custom resolver: a Docker Socket Client example\n\nCohttp provides a lot of utilities out of the box, but does not prevent the users\nto dig in and customise it for their needs. The following is an example of a\n[unix socket client to communicate with Docker](https://discuss.ocaml.org/t/how-to-write-a-simple-socket-based-web-client-for-docker/1760/3).\n\n```ocaml\nopen Lwt.Infix\nopen Cohttp\n\nlet ctx =\n  let resolver =\n    let h = Hashtbl.create 1 in\n    Hashtbl.add h \"docker\" (`Unix_domain_socket \"/var/run/docker.sock\");\n    Resolver_lwt_unix.static h\n  in\n  Cohttp_lwt_unix.Client.custom_ctx ~resolver ()\n\nlet t =\n  Cohttp_lwt_unix.Client.get ~ctx (Uri.of_string \"http://docker/version\")\n  \u003e\u003e= fun (resp, body) -\u003e\n  let open Cohttp in\n  let code = resp |\u003e Response.status |\u003e Code.code_of_status in\n  Printf.printf \"Response code: %d\\n\" code;\n  Printf.printf \"Headers: %s\\n\" (resp |\u003e Response.headers |\u003e Header.to_string);\n  body |\u003e Cohttp_lwt.Body.to_string \u003e|= fun body -\u003e\n  Printf.printf \"Body of length: %d\\n\" (String.length body);\n  print_endline (\"Received body\\n\" ^ body)\n\nlet _ = Lwt_main.run t\n```\n\nThe main issue there is there no way to resolve a socket address, so you need to\ncreate a custom resolver to map a hostname to the Unix domain socket.\n\nTo build and execute with `dune`, first create the following `dune` file\n```\n$ cat - \u003e dune \u003c\u003cEOF\n(executable\n (public_name docker_example)\n (name docker_example)\n (libraries cohttp-lwt-unix conduit-lwt))\nEOF\n```\nthen run the example with\n```\n$ dune exec ./docker_example.exe\n```\nEven though conduit is transitively there, for this example we are explicitly\nmentioning it to emphasize that we are creating a new Conduit resolver. Refer to\n[conduit's README](https://github.com/mirage/ocaml-conduit/) for examples of use and\nlinks to up-to-date conduit documentation.\n\n## Dealing with redirects\n\nThis examples has been adapted from a script on the [ocaml.org](https://github.com/ocaml/ocaml.org/blob/master/script/http.ml) website, and shows an explicit way to deal with redirects in `cohttp-lwt-unix`.\n\n```ocaml\nlet rec http_get_and_follow ~max_redirects uri =\n  let open Lwt.Syntax in\n  let* ans = Cohttp_lwt_unix.Client.get uri in\n  follow_redirect ~max_redirects uri ans\n\nand follow_redirect ~max_redirects request_uri (response, body) =\n  let open Lwt.Syntax in\n  let status = Http.Response.status response in\n  (* The unconsumed body would otherwise leak memory *)\n  let* () =\n    if status \u003c\u003e `OK then Cohttp_lwt.Body.drain_body body else Lwt.return_unit\n  in\n  match status with\n  | `OK -\u003e Lwt.return (response, body)\n  | `Permanent_redirect | `Moved_permanently -\u003e\n      handle_redirect ~permanent:true ~max_redirects request_uri response\n  | `Found | `Temporary_redirect -\u003e\n      handle_redirect ~permanent:false ~max_redirects request_uri response\n  | `Not_found | `Gone -\u003e failwith \"Not found\"\n  | status -\u003e\n      Printf.ksprintf failwith \"Unhandled status: %s\"\n          (Cohttp.Code.string_of_status status)\n\nand handle_redirect ~permanent ~max_redirects request_uri response =\n  if max_redirects \u003c= 0 then failwith \"Too many redirects\"\n  else\n    let headers = Http.Response.headers response in\n    let location = Http.Header.get headers \"location\" in\n    match location with\n    | None -\u003e failwith \"Redirection without Location header\"\n    | Some url -\u003e\n        let open Lwt.Syntax in\n        let uri = Uri.of_string url in\n        let* () =\n          if permanent then\n            Logs_lwt.warn (fun m -\u003e\n                m \"Permanent redirection from %s to %s\"\n                  (Uri.to_string request_uri)\n                  url)\n          else Lwt.return_unit\n        in\n        http_get_and_follow uri ~max_redirects:(max_redirects - 1)\n```\n\nThe following example, adapted from [blue-http](https://github.com/brendanlong/blue-http/blob/master/src/redirect.ml), does a similar thing with `cohttp-async` (and [ppx_let](https://github.com/janestreet/ppx_let)).\n\n```ocaml\nopen Core_kernel\nopen Async_kernel\n\nlet with_redirects ~max_redirects uri f =\n  let seen_uris = Hash_set.create (module String) in\n  let rec loop ~max_redirects uri =\n    Hash_set.add seen_uris (Uri.to_string uri);\n    let%bind ((response, response_body) as res) = f uri in\n    let status_code =\n      Cohttp.(Response.status response |\u003e Code.code_of_status)\n    in\n    if Cohttp.Code.is_redirection status_code then (\n      match Cohttp.(Response.headers response |\u003e Header.get_location) with\n      | Some new_uri when Uri.to_string new_uri |\u003e Hash_set.mem seen_uris -\u003e\n          return res\n      | Some new_uri -\u003e\n          if max_redirects \u003e 0 then\n            (* Cohttp leaks connections if we don't drain the response body *)\n            Cohttp_async.Body.drain response_body \u003e\u003e= fun () -\u003e\n            loop ~max_redirects:(max_redirects - 1) new_uri\n          else (\n            Log.Global.debug ~tags:[]\n              \"Ignoring %d redirect from %s to %s: redirect limit exceeded\"\n              status_code (Uri.to_string uri) (Uri.to_string new_uri);\n            return res)\n      | None -\u003e\n          Log.Global.debug ~tags:[]\n            \"Ignoring %d redirect from %s: there is no Location header\"\n            status_code (Uri.to_string uri);\n          return res)\n    else return res\n  in\n  loop ~max_redirects uri\n```\n\nYou can read a bit more on the rationale behind the absence of this functionality in the API [here](https://github.com/mirage/ocaml-cohttp/issues/76).\n\n## Basic Server Tutorial\n\nImplementing a server in cohttp using the Lwt backend (for Async is very similar)\nis mostly equivalent to implementing a function of type :\n\n```\nconn -\u003e Http.Request.t -\u003e Cohttp_lwt.Body.t -\u003e (Http.Response.t * Cohttp_lwt.Body.t) Lwt.t\n```\n\nThe parameters are self explanatory but we'll summarize them quickly here:\n\n* `conn` - contains connection information\n* `Http.Request.t` - Request information such as method, uri, headers, etc.\n* `Cohttp_lwt.Body.t` - Contains the request body. You must manually decode the\n  request body into json, form encoded pairs, etc. For cohttp, the body is\n  simply binary data.\n\nHere's an example of a simple cohttp server that outputs back request\ninformation.\n\n```ocaml\nopen Lwt\nopen Cohttp\nopen Cohttp_lwt_unix\n\nlet server =\n  let callback _conn req body =\n    let uri = req |\u003e Request.uri |\u003e Uri.to_string in\n    let meth = req |\u003e Request.meth |\u003e Code.string_of_method in\n    let headers = req |\u003e Request.headers |\u003e Header.to_string in\n    ( body |\u003e Cohttp_lwt.Body.to_string \u003e|= fun body -\u003e\n      Printf.sprintf \"Uri: %s\\nMethod: %s\\nHeaders\\nHeaders: %s\\nBody: %s\" uri\n        meth headers body )\n    \u003e\u003e= fun body -\u003e Server.respond_string ~status:`OK ~body ()\n  in\n  Server.create ~mode:(`TCP (`Port 8000)) (Server.make ~callback ())\n\nlet () = ignore (Lwt_main.run server)\n```\n\n### Compile and execute with dune\n\nCreate this `dune` file\n```\ncat - \u003e dune \u003c\u003cEOF\n(executable\n (public_name server_example)\n (name server_example)\n (libraries cohttp-lwt-unix conduit-lwt))\nEOF\n```\nthen build and execute the example with\n```\n$ dune exec ./server_example.exe\n```\n\nAs in the previous example, here we are explicitly mentioning conduit-lwt to\nemphasize that we are relying on Conduit to specify the protocols and the\nservices. Refer to [conduit's README](https://github.com/mirage/ocaml-conduit/)\nfor examples of use and links to up-to-date conduit documentation.\n\n\n## Installed Binaries\n\nCohttp comes with a few simple binaries that are handy, useful also to test cohttp\nitself, and can serve as examples of how to use the library. All binaries come in two\nflavours - Async and Lwt.\n\n* `$ cohttp-curl-{lwt,async}`\n\nThis is a simple curl utility implemented using cohttp. An example of an\ninvocation is:\n\n```\n$ cohttp-curl-lwt -v -X GET \"https://www.reddit.com/\"\n```\n\n* `$ cohttp-server-{lwt,async}`\n\nThis binary acts in a similar fashion to the Python `SimpleHTTPServer`. Just\nrun `cohttp-server-async` in a directory and it will open up a local port and\nserve the files over HTTP.\n\n```\n$ cohttp-server-async\n```\n\nAssuming that the server is running in cohttp's source directory:\n\n```\n$ cohttp-curl-lwt 'http://0.0.0.0:8080/README.md'\n```\n\nOther examples using the async api are available in the\n[cohttp-async/examples](https://github.com/mirage/ocaml-cohttp/tree/master/cohttp-async/examples)\nfolder in the sources.\n\n## Debugging\n\nYou can activate some runtime debugging for the servers by setting `COHTTP_DEBUG` to any value different from `0` or `false`, and it will set a default debug-level logger on stdout.\n\nSince both Cohttp and Conduit use `Logs` for debugging output, you can enable custom debugging in your code (if needed). For example, if you intend to make use of the `COHTTP_DEBUG` env variable, you could simply use\n\n```ocaml\nlet () =\n  if not @@ Debug.debug_active () then (\n    Fmt_tty.setup_std_outputs ();\n    Logs.set_level ~all:true level;\n    Logs.set_reporter Debug.default_reporter);\n```\n\nOf course you are free to completely override it and use your own reporters, for example by adding something like the following to your code (courtesy of @dinosaure).\n\n```ocaml\nlet reporter ppf =\n  let report src level ~over k msgf =\n    let k _ =\n      over () ;\n      k () in\n    let with_metadata header _tags k ppf fmt =\n      Format.kfprintf k ppf\n        (\"%a[%a]: \" ^^ fmt ^^ \"\\n%!\")\n        Logs_fmt.pp_header (level, header)\n        Fmt.(styled `Magenta string)\n        (Logs.Src.name src) in\n    msgf @@ fun ?header ?tags fmt -\u003e with_metadata header tags k ppf fmt in\n  { Logs.report }\n\nlet () =\n  Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ();\n  Logs.set_reporter (reporter Fmt.stderr);\n  Logs.set_level ~all:true (Some Logs.Debug)\n```\n\nNote that you can selectively filter out the logs produced by `cohttp-lwt` and `cohttp-lwt-unix` internals as follows.\n\n```ocaml\nlet () =\n  (* Set log level v for all loggers, this does also affect cohttp internal loggers *)\n  Logs.set_level ~all:true level;\n  (* Disable all cohttp-lwt and cohttp-lwt-unix logs *)\n  List.iter (fun src -\u003e\n      match Logs.Src.name src with\n      | \"cohttp.lwt.io\" | \"cohttp.lwt.server\" -\u003e Logs.Src.set_level src None\n      | _ -\u003e ())\n  @@ Logs.Src.list ()\n```\n\n## Important Links\n\n- [Cohttp API Documentation](https://mirage.github.io/ocaml-cohttp/)\n- [Conduit API Documentation](https://mirage.github.io/ocaml-conduit/)\n","funding_links":[],"categories":["OCaml","Libraries","Networking","\u003ca name=\"OCaml\"\u003e\u003c/a\u003eOCaml"],"sub_categories":["Web Frameworks"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmirage%2Focaml-cohttp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmirage%2Focaml-cohttp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmirage%2Focaml-cohttp/lists"}