katipo

December 28, 2025 ยท View on GitHub

An HTTP/HTTP2/HTTP3 client library for Erlang built around libcurl-multi and libevent.

Status

build status Hex pm Hex Docs

Usage

{ok, _} = application:ensure_all_started(katipo).
Pool = api_server,
{ok, _} = katipo_pool:start(Pool, 2, [{pipelining, multiplex}]).
Url = <<"https://example.com">>.
ReqHeaders = [{<<"User-Agent">>, <<"katipo">>}].
Opts = #{headers => ReqHeaders,
         body => <<"0d5cb3c25b0c5678d5297efa448e1938">>,
         connecttimeout_ms => 5000,
         proxy => <<"http://127.0.0.1:9000">>,
         ssl_verifyhost => false,
         ssl_verifypeer => false},
{ok, #{status := 200,
       headers := RespHeaders,
       cookiejar := CookieJar,
       body := RespBody}} = katipo:post(Pool, Url, Opts).

Or passing the entire request as a map

{ok, _} = application:ensure_all_started(katipo).
Pool = api_server,
{ok, _} = katipo_pool:start(Pool, 2, [{pipelining, multiplex}]).
ReqHeaders = [{<<"User-Agent">>, <<"katipo">>}].
Req = #{url => <<"https://example.com">>,
        method => post,
        headers => ReqHeaders,
        body => <<"0d5cb3c25b0c5678d5297efa448e1938">>,
        connecttimeout_ms => 5000,
        proxy => <<"http://127.0.0.1:9000">>,
        ssl_verifyhost => false,
        ssl_verifypeer => false},
{ok, #{status := 200,
       headers := RespHeaders,
       cookiejar := CookieJar,
       body := RespBody}} = katipo:req(Pool, Req).

Why

We wanted a compatible and high-performance HTTP client so took advantage of the 25+ years of development that has gone into libcurl. To allow large numbers of simultaneous connections libevent is used along with the libcurl-multi interface.

Documentation

API

-type method() :: get | post | put | head | options | patch | delete.
katipo_pool:start(Name :: atom(), size :: pos_integer(), PoolOptions :: proplist()).
katipo_pool:stop(Name :: atom()).

katipo:req(Pool :: atom(), Req :: map()).
katipo:Method(Pool :: atom(), URL :: binary()).
katipo:Method(Pool :: atom(), URL :: binary(), ReqOptions :: map()).

Request options

OptionTypeDefaultNotes
headers[{binary(), iodata()}][]
cookiejaropaque (returned in response)[]
bodyiodata()<<>>
connecttimeout_mspos_integer()30000docs
followlocationboolean()falsedocs
ssl_verifyhostboolean()truedocs
ssl_verifypeerboolean()truedocs
capathbinary()undefineddocs
cacertbinary()undefineddocs
ca_cache_timeoutinteger()86400docs curl >= 7.87.0 (0=disable, -1=forever)
timeout_mspos_integer()30000docs
dns_cache_timeoutinteger()60docs (0=disable, -1=forever)
maxredirsnon_neg_integer()9docs
proxybinary()undefineddocs
tcp_fastopenboolean()falsedocs curl >= 7.49.0
pipewaitboolean()truedocs curl >= 7.43.0
interfacebinary()undefineddocs
unix_socket_pathbinary()undefineddocs curl >= 7.40.0
doh_urlbinary()undefineddocs curl >= 7.62.0
http_versioncurl_http_version_none
curl_http_version_1_0
curl_http_version_1_1
curl_http_version_2_0
curl_http_version_2tls
curl_http_version_2_prior_knowledge
curl_http_version_3
curl_http_version_nonedocs HTTP/3 requires curl >= 7.66.0
sslversionsslversion_default
sslversion_tlsv1
sslversion_tlsv1_0
sslversion_tlsv1_1
sslversion_tlsv1_2
sslversion_tlsv1_3
sslversion_defaultdocs
sslcertbinary()undefineddocs
sslkeybinary()undefineddocs
sslkey_blobbinary() (DER format)undefineddocs curl >= 7.71.0
keypasswdbinary()undefineddocs
http_authbasic
digest
ntlm
negotiate
undefineddocs
usernamebinary()undefineddocs
passwordbinary()undefineddocs
userpwdbinary()undefineddocs
verboseboolean()falsedocs

Responses

{ok, #{status := pos_integer(),
       headers := headers(),
       cookiejar := cookiejar(),
       body := body()}}

{error, #{code := atom(), message := binary()}}

Pool Options

OptionTypeDefaultNote
pipeliningnothing
http1
multiplex
nothingHTTP pipelining CURLMOPT_PIPELINING
max_pipeline_lengthnon_neg_integer()100
max_total_connectionsnon_neg_integer()0 (no limit)docs
max_concurrent_streamsnon_neg_integer()100docs curl >= 7.67.0

Observability

Katipo uses OpenTelemetry for tracing and metrics.

Tracing

Each HTTP request creates a span with the following attributes:

AttributeDescription
http.request.methodHTTP method (GET, POST, etc.)
url.fullRequest URL (query string, fragment and userinfo are stripped for security)
server.addressTarget host
http.response.status_codeResponse status code (on success)
Metrics

The following metrics are recorded:

MetricTypeDescription
http.client.requestsCounterNumber of HTTP requests (with result and http.response.status_code attributes)
http.client.durationHistogramTotal request duration (ms)
http.client.curl_timeHistogramCurl total time (ms)
http.client.namelookup_timeHistogramDNS lookup time (ms)
http.client.connect_timeHistogramConnection time (ms)
http.client.appconnect_timeHistogramSSL/TLS handshake time (ms)
http.client.pretransfer_timeHistogramPre-transfer time (ms)
http.client.redirect_timeHistogramRedirect processing time (ms)
http.client.starttransfer_timeHistogramTime to first byte (ms)

All histogram metrics include the http.request.method attribute for filtering by HTTP method.

Enabling OpenTelemetry Export

The OpenTelemetry API is a no-op by default. To export telemetry data add the OpenTelemetry SDK and an exporter to your release:

%% In rebar.config
{deps, [
    {opentelemetry, "1.5.0"},
    {opentelemetry_experimental, "0.5.1"},
    {opentelemetry_exporter, "1.8.0"}
]}.

Configure the exporter in your sys.config:

[
 {opentelemetry, [
   {span_processor, batch},
   {traces_exporter, otlp}
 ]},
 {opentelemetry_experimental, [
   {readers, [
     #{module => otel_metric_reader,
       config => #{exporter => {opentelemetry_exporter, #{}}}}
   ]}
 ]},
 {opentelemetry_exporter, [
   {otlp_endpoint, "http://localhost:4318"}
 ]}
].
Migration from metrics library

If you were using the previous metrics library integration, note the following breaking changes:

  • The mod_metrics application environment option has been removed
  • The return_metrics request option has been removed
  • The metrics field is no longer included in response maps

To access timing metrics, configure an OpenTelemetry exporter as shown above. The histogram metrics provide the same timing data (DNS lookup, connect time, TLS handshake, etc.) that was previously available via return_metrics.

System dependencies

  • libevent-dev
  • libcurl4-openssl-dev
  • make
  • curl
  • libssl-dev
  • gcc

Testing

The official Erlang Docker image has everything needed to build and test Katipo.

Local httpbin Setup

The test suite uses a local httpbin instance running behind Caddy (for HTTPS/HTTP2/HTTP3 support).

Start the httpbin container:

cd test/http3-httpbin
docker-compose up -d

This starts:

  • httpbin: A local instance of the httpbin.org API
  • Caddy: Reverse proxy providing HTTPS with auto-generated self-signed certificates on port 8443

Run the tests (requires httpbin to be running):

rebar3 ct

To run with coverage:

rebar3 ct --cover
rebar3 cover --verbose

Stop the containers when done:

cd test/http3-httpbin
docker-compose down

Feature Availability

Some features are only available with newer versions of libcurl. You can check availability at runtime:

katipo:tcp_fastopen_available().    %% curl >= 7.49.0
katipo:unix_socket_path_available(). %% curl >= 7.40.0
katipo:doh_url_available().          %% curl >= 7.62.0
katipo:sslkey_blob_available().      %% curl >= 7.71.0
katipo:http3_available().            %% curl >= 7.66.0