A requests like HTTP client for Mojo, leveraging libcurl under the hood.
import floki
def main() raises -> None:
var response = floki.get("https://example.com")
for pair in response.headers.items():
print(pair.key, ": ", pair.value)
print(response.as_text())You'll need to enable the pixi-build preview by adding this to the workspace section of your pixi.toml file.
preview = ["pixi-build"]There's two ways to build floki from source: directly from the Git repository or by cloning the repository locally.
Run the following commands in your terminal:
pixi add floki --git "https://github.com/thatstoasty/floki.git" --tag v0.3.4 && pixi install# Clone the repository to your local machine
git clone https://github.com/thatstoasty/floki.git
# Add the package to your project from the local path
pixi add -s ./path/to/floki && pixi installNote: Mojo cannot currently support calling C functions with variadic arguments, and the libcurl client interface makes heavy use of them. Floki uses my small shim C library
curl_wrapper, which provides a thin wrapper around libcurl to avoid this issue.
Floki leverages mojo-curl as an FFI interface to libcurl, and it needs to locate two dynamic libraries at runtime: libcurl and libcurl_wrapper (the thin C shim that wraps libcurl's variadic functions).
By default, the library looks for both in your project's Pixi environment:
| Library | macOS | Linux |
|---|---|---|
| libcurl | .pixi/envs/default/lib/libcurl.dylib |
.pixi/envs/default/lib/libcurl.so |
| curl_wrapper | .pixi/envs/default/lib/libcurl_wrapper.dylib |
.pixi/envs/default/lib/libcurl_wrapper.so |
If you're working in a Pixi environment, the libraries will already be in the expected location and no additional configuration is needed.
If your libraries are in a different location, you can override the paths using either environment variables or compile-time defines.
Set LIBCURL_LIB_PATH and CURL_WRAPPER_LIB_PATH before running your program:
export LIBCURL_LIB_PATH="/usr/lib/libcurl.so"
export CURL_WRAPPER_LIB_PATH="/opt/mylibs/libcurl_wrapper.so"
mojo run my_program.mojoPass the paths as -D flags when compiling or running:
mojo run -D LIBCURL_LIB_PATH="/usr/lib/libcurl.so" -D CURL_WRAPPER_LIB_PATH="/opt/mylibs/libcurl_wrapper.so" my_program.mojoCompile-time defines take priority over environment variables. If neither is set, the default Pixi environment paths are used.
Floki aims to provide a requests-like experience on top of libcurl.
- All standard HTTP methods:
get,post,put,patch,delete,head, andoptions. - One-shot free functions (
floki.get(...)) or a reusableSessionthat persists configuration across requests. - Request bodies from JSON, form-encoded data, raw bytes, files, or your own structs.
- Authentication helpers (Basic, Bearer) plus a pluggable
Authtrait for custom schemes. - Query parameters, custom headers, and automatic redirect handling.
- Cookie parsing and a cookie jar.
- Ergonomic responses: status-class checks, body as text/bytes/typed or dynamic JSON, and case-insensitive headers.
- Session-level timeout, retry-with-backoff, proxy, and TLS verification controls.
Every method is available both as a free function and as a Session method:
import floki
from floki.session import Session
def main() raises -> None:
var r = floki.get("https://httpbin.org/get")
var session = Session()
var r2 = session.delete("https://httpbin.org/delete")Pass a dictionary literal to send JSON, or wrap it in FormData for
application/x-www-form-urlencoded:
import floki
from floki.forms import FormData
def main() raises -> None:
# JSON body (Content-Type: application/json)
var r = floki.post("https://httpbin.org/post", data={"key": "value"})
# Form-encoded body (Content-Type: application/x-www-form-urlencoded)
var r2 = floki.post("https://httpbin.org/post", data=FormData({"key": "value"}))Raw bytes, file handles, and arbitrary Writable structs can also be sent as the body.
A Response exposes the status, headers, and body, with convenience accessors for
the common cases:
import floki
def main() raises -> None:
var r = floki.get("https://httpbin.org/get")
# Status checks by class (1xx-5xx), plus the exact `is_ok` (200 only).
if r.is_success(): # any 2xx
print(r.status.code, r.reason())
if r.is_client_error() or r.is_server_error():
r.raise_for_status() # raises HTTPError on a non-2xx response
# Body as text, raw bytes, or JSON.
print(r.as_text()) # StringSlice over the body
var data = r.as_json() # dynamic JSON, e.g. data["url"]
# Headers are looked up case-insensitively.
print(r.content_type()) # value of the Content-Type header
print(r.header("x-request-id", "<none>"))For typed JSON, deserialize straight into a struct with r.as[T]():
import floki
@fieldwise_init
struct Todo(Defaultable, ImplicitlyDestructible, Movable):
var id: Int
var title: String
def __init__(out self):
self.id = 0
self.title = ""
def main() raises -> None:
var r = floki.get("https://jsonplaceholder.typicode.com/todos/1")
var todo = r.as[Todo]()
print(todo.id, todo.title)raise_for_status() treats any 2xx as success, so 201 Created and 204 No Content
do not raise.
Basic and Bearer helpers are built in, and any type implementing the Auth trait
can supply its own headers. Authentication is provided per request:
import floki
from floki import Headers
from floki.auth import Auth, BasicAuth, BearerAuth
def main() raises -> None:
var r = floki.get("https://httpbin.org/basic-auth/user/pass", auth=BasicAuth("user", "pass"))
var r2 = floki.get("https://api.example.com/me", auth=BearerAuth("my-token"))
# Custom scheme: implement `apply` to set whatever headers you need.
@fieldwise_init
struct ApiKeyAuth(Auth):
var key: String
def apply(self, mut headers: Headers):
headers["X-Api-Key"] = self.keyA header you set explicitly on the request always takes precedence over the auth argument.
import floki
def main() raises -> None:
var r = floki.get("https://httpbin.org/get", query_parameters={"q": "mojo", "page": "1"})Configure granular connect/total timeouts (in seconds) on a Session. A bare number
is treated as the total timeout:
from floki.session import Session
from floki.timeout import Timeout
def main() raises -> None:
var session = Session(timeout=Timeout(connect=5.0, total=30.0))
var quick = Session(timeout=10) # 10 second total timeout
var r = session.get("https://example.com")Retry failed transfers and retryable status codes with exponential backoff:
from floki.session import Session
from floki.retry import Retry
def main() raises -> None:
var session = Session(
retry=Retry(max_retries=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])
)
var r = session.get("https://example.com")The delay before retry n is backoff_factor * 2 ** (n - 1) seconds.
from floki.session import Session
from floki.proxy import Proxy
def main() raises -> None:
var session = Session(proxy=Proxy("http://proxy.example:8080"))
# With credentials and a bypass list:
var authed = Session(
proxy=Proxy(
"http://proxy.example:8080",
username="user",
password="secret",
no_proxy="localhost,127.0.0.1",
)
)
var r = session.get("https://example.com")TLS certificate and hostname verification is enabled by default. It can be disabled (use with great caution) or pointed at a custom certificate authority bundle:
from std.pathlib import Path
from floki.session import Session
from floki.tls import TLS
def main() raises -> None:
# Disable verification — only for testing or trusted networks.
var insecure = Session(tls=TLS(verify=False))
# Use a custom CA bundle (e.g. a private PKI or self-signed cert).
var custom = Session(tls=TLS(ca_bundle=Path("/path/to/ca-bundle.pem")))
var r = custom.get("https://internal.example.com")
timeout,retry,proxy, andtlsare also accepted directly by the free functions (e.g.floki.get(url, timeout=10)), which forward them to the one-shotSessionthey create internally.
- Add an option for streaming responses instead of loading it all into memory.
- Cleanup cookie parsing code, it seems pretty slow.
- Sus out the myriad of bugs and edge cases that may arise as libcurl and requests can do A LOT of things, that I've never used before. Please open issues and open PRs to help address these gaps where possible.
- Add methods to free Session explicitly, same with Easy handles.
- Add support for passing Dict data to session methods. Just passing a dict literal is a little limiting. I've tried, but it gets very hairy trying to convert it to an emberjson JSON object.
Reminder, this is a hobby project! You're free to fork it and make changes as you see fit.