httpp
from httpp import Server
app = Server()
@app.get("/hello/{name}")
def hello(name):
return f"hello {name}"
app.listen("127.0.0.1", 8000) # that's it — no separate server process needed
pip install httpp gives you a Python package, a command-line tool, and a compiled C++ library with
headers and a CMake config. Every third-party component is built from source, so nothing has to be
installed system-wide (no apt, brew or choco).
Contents
- Features
- Install
- Python
- Command line
- C++
- Migrating from libcurl
- Using httpp from CMake
- Examples
- Build from source
- Status
- License
Features
- Client and server in one library. Most lightweight HTTP libraries only do one side.
- Decorator-style routing in Python, in the spirit of Flask and FastAPI: path parameters, typed
conversion, JSON responses,
async defhandlers. - Built-in server. No ASGI/WSGI layer and no separate process to launch.
- Real async.
async defhandlers run on one shared asyncio event loop and release their worker thread while theyawait. Idle connections wait in an event loop too (epoll, kqueue or IOCP, via Asio), so thousands of slow or idle requests cost almost no threads. - Python, C++ and a CLI from the same wheel. One binary core, three front ends.
- HTTP and HTTPS on the client (mbedtls, vendored).
- Downloads with a tqdm-style progress bar (drawn on stderr, so piped output stays clean).
- A migration path for
libcurlcode:httpp/curl_compat.hprovides acurl_easy_*subset. - No system dependencies. Nothing to install system-wide; the only build-time download is the header-only Asio (about 3 MB, pinned by hash), which can be supplied locally instead.
Install
pip install httpp
Requires Python 3.8 or newer. Building the C++ side on its own needs CMake 3.21+ and a C++17 compiler (see Build from source).
Python
Serving: routes
Register handlers with decorators, then call listen():
import asyncio
from httpp import Server
app = Server()
@app.get("/hello/{name}") # "/hello/<name>" is accepted too
def hello(name):
return f"hello {name}"
@app.get("/square/{n}")
def square(n: int): # annotate as int/float to get a converted value
return {"n": n, "square": n * n} # dict/list -> JSON
@app.post("/items")
def create_item(request): # ask for `request` by name
return {"got": request.json()}, 201, {"Location": "/items/1"}
@app.get("/slow")
async def slow(): # async handlers: no thread is held while awaiting
await asyncio.sleep(1)
return "done"
app.listen("127.0.0.1", 8000)
All of get, post, put, patch and delete are available, and route() takes a list of methods
(@app.route("/thing", methods=["PUT", "PATCH"])). Register routes before calling listen().
Static files can be served next to your routes:
app.serve_directory("/static", "./public")
Handler reference
A handler may declare any of its path parameters by name, plus request. It returns one of:
| Return value | Response |
|---|---|
str |
200, text/plain; charset=utf-8 |
bytes |
200, application/octet-stream |
dict / list |
200, application/json |
None |
200, empty body |
(body, status) |
same, with your status code |
(body, status, headers) |
same, with extra headers (a Content-Type you set wins) |
The request object:
| Attribute | Meaning |
|---|---|
method, path |
HTTP method and path |
body / text / json() |
raw bytes, decoded text, parsed JSON |
headers |
dict, names lower-cased (request.headers["x-token"]) |
query |
dict of ?a=1 parameters |
path_params |
dict of captured path parameters (all str) |
For repeated headers or query names the first value wins.
Error behavior:
- Return a status yourself for expected failures:
return {"error": "not found"}, 404. - A path parameter annotated
int/floatthat does not parse answers 422 automatically. - An uncaught exception answers a generic 500; the traceback goes to stderr, never to the client.
Running the server
listen() is the launcher: python app.py is all it takes.
if __name__ == "__main__":
app.listen("0.0.0.0", 8000)
- It blocks until
stop()is called or you press Ctrl+C. - It raises
OSErrorif the address cannot be bound (for example, the port is already taken). Two httpp servers cannot share a port. - Plain
defhandlers run on the server's thread pool (at most a few dozen threads, depending on the machine), so a handler that blocks holds its thread for as long as it blocks. async defhandlers run on one shared asyncio event loop (a background thread, started on first use) and give their worker thread back as soon as they start. While theyawait, no thread is held, so far more requests can be in flight than there are threads: on a 1-core test machine, 400 concurrent requests that eachawait asyncio.sleep(0.5)all finish in about 0.7 s. Because every request shares the same loop, loop-bound objects (anaiohttp.ClientSession, an async DB pool) can be created once and reused.- Connections are event-driven on every platform: idle connections, including HTTP keep-alive ones, wait in an Asio event loop (epoll on Linux, kqueue on macOS, IOCP on Windows) on the listening thread and cost no worker thread. A worker is used only while a request is actually being read and answered, then the connection goes back to the loop. On Linux, 3000 idle connections were held by that one thread and a request still took 0.2 ms, the same as with none. This also applies to async routes.
- Keep-alive after a response needs the loop to take the socket back, which Windows supports from 8.1 on.
On older Windows (and under Wine) the loop notices this and answers
Connection: close: requests are still served and idle connections still cost no thread, there is just no connection reuse. - CPU-bound handlers are still limited by the GIL, async or not.
- A request that has only partly arrived holds a worker thread until it completes or times out.
There is no ASGI/WSGI interface, so servers such as uvicorn or gunicorn do not apply, and there is no auto-reload or multi-process mode.
Client
from httpp import Client
res = Client.fetch("http://example.com/") # full URL in one call
print(res.status, res.ok) # 200 True
print(res.body) # body as str
print(res.header("content-type")) # case-insensitive, or None
print(res.headers) # [(name, value), ...]
client = Client("example.com", 80) # or host + port
print(client.get("/index.html").status)
A non-2xx status is not an exception; check res.ok or res.status.
Request builder
A small, curl-flavored fluent builder for the common cases (-X, -H, -d):
from httpp import Request
res = (
Request("http://example.com/api")
.method("POST")
.header("X-Token", "abc")
.content_type("application/json")
.data('{"a": 1}')
.run()
)
Without .content_type(), data is sent as application/x-www-form-urlencoded, like curl -d.
Downloads
from httpp import download, Download
result = download("http://example.com/big.zip", "big.zip") # progress bar on stderr
result = download(url, "big.zip", show_progress=False)
result = Download(url).output("big.zip").disable_progress().run()
if not result.ok:
print(result.status, result.error)
Command line
httpp server . # serve a directory, like `python -m http.server`
httpp server ./public --host 127.0.0.1 --port 9000
httpp download http://example.com/ # print the body to stdout
httpp download http://example.com/f.zip -o f.zip # save to a file, with a progress bar
httpp curl -X POST -H "X-Token: abc" -d "a=1" http://example.com/api
httpp install --user # install headers, library and CMake config
httpp path --include-dir # print include/lib/cmake locations
C++
The same server, client and download builder are available natively. Include the umbrella header:
#include <httpp.h>
#include <chrono>
#include <thread>
// Server
httpp::server srv;
srv.get("/hello", [](const httpp::request&, httpp::response& res) {
res.body = "world"; // status defaults to 200
});
srv.post("/users/:id", [](const httpp::request& req, httpp::response& res) {
// req.method, req.path, req.body
// req.headers, req.query, req.path_params: vectors of (name, value) pairs
res.status = 201;
res.headers.emplace_back("Content-Type", "application/json");
res.body = R"({"ok": true})";
});
// Async: the handler gets a `responder` and may return at once, releasing the worker
// thread; call send() later, from any thread (an event loop, a timer, a callback).
srv.route_async("GET", "/slow", [](const httpp::request&, httpp::responder r) {
std::thread([r] { // stand-in for "wait for something"
std::this_thread::sleep_for(std::chrono::seconds(1));
httpp::response res;
res.status = 200;
res.body = "done";
r.send(res); // thread-safe; first call wins
}).detach();
});
srv.serve_directory("/static", "./public");
srv.listen("0.0.0.0", 8000); // false if it could not bind; true after stop()
// Client
auto res = httpp::client::fetch("http://example.com/");
auto res2 = httpp::client::request("http://example.com/api")
.method("POST")
.header("X-Token", "abc")
.data("a=1")
.timeout(10)
.follow_redirects()
.run();
// Download
auto result = httpp::download("http://example.com/f.zip").output("f.zip").resume().run();
get, post, put, patch and del (delete is a C++ keyword) are shorthands for the general
route(method, path, handler), which accepts GET, POST, PUT, PATCH, DELETE and OPTIONS
(case-insensitive) and throws std::invalid_argument for anything else. :name segments in the path are captured into
request::path_params.
route_async() is the non-blocking variant: its handler receives a responder instead of a response&.
An exception thrown by it is answered with a plain 500; dropping every copy of the responder without
calling send() closes the connection unanswered; and if the server is destroyed first, pending
connections are closed and a later send() does nothing.
The Python decorators are a thin layer on top of these same route() / route_async() calls.
Migrating from libcurl
Switch the header and keep most existing curl_easy_* code:
#include <httpp/curl_compat.h> /* was: #include <curl/curl.h> */
CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out);
CURLcode rc = curl_easy_perform(curl);
curl_easy_cleanup(curl);
It covers the common subset (URL, method, headers, POST data, write and header callbacks, timeout,
redirect following, response code, content type). It is not a full libcurl replacement; the comment at
the top of curl_compat.h lists exactly what is supported.
Using httpp from CMake
No system-wide install is needed. Point CMake at the package that pip already installed:
execute_process(
COMMAND python3 -c "import httpp; print(httpp.get_cmake_dir())"
OUTPUT_VARIABLE httpp_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
find_package(httpp CONFIG REQUIRED)
target_link_libraries(app PRIVATE httpp::httpp_core)
Alternatively, run httpp install [--user] once and a plain find_package(httpp) works with no Python
involved. A complete consumer project lives in tests/cmake/consumer_project.
Examples
Standalone, runnable scripts, one feature each, in examples/python/. They assume
only pip install httpp.
| Example | Shows |
|---|---|
01_hello_server.py |
the smallest server |
02_path_params.py |
{name} / <name>, int conversion, 422 |
03_request_object.py |
query, headers, JSON body |
04_responses.py |
text, bytes, JSON, HTML, status, headers |
05_http_methods.py |
an in-memory CRUD API |
06_async_handlers.py |
async def handlers |
07_serve_directory.py |
static files next to routes |
08_error_handling.py |
404, 500, port already in use |
09_client.py |
Client.get and Client.fetch |
10_request_builder.py |
method, headers, body |
11_download.py |
downloads and progress |
The client examples (09 to 11) start their own local server, so they run offline.
Build from source
git clone --recurse-submodules https://github.com/MohammadRaziei/httpp.git
cd httpp
pip install -r requirements-dev.txt
cmake -B build -DHTTPP_BUILD_TESTS=ON -DHTTPP_BUILD_PYTHON=ON
cmake --build build -j4
ctest --test-dir build --output-on-failure # C++, Python and CMake-integration tests
Most dependencies are vendored (cpp-httplib, which carries two patches) or git submodules (mbedtls,
liburlparser). The full history of the mbedtls submodule is large; if you only need the source, clone
with git clone --depth 1 --recurse-submodules --shallow-submodules.
Asio (header-only, used unmodified) is found in this order:
-DHTTPP_ASIO_INCLUDE_DIR=<dir containing asio.hpp>, then a copy in src/third_party/asio (for example a
git submodule), then it is downloaded at configure time (about 3 MB, pinned to 1.34.2 by SHA-256). Offline
builds should pass the first option. See src/third_party/README.md.
Status
httpp is alpha: the API can still change between minor versions. Current limitations:
- The Python
Requestbuilder supports method, headers, body and content type; timeouts, redirect control, resumable downloads and async downloads are C++-only for now. - A request that has only partly arrived (a slow client still sending its headers or body) holds a worker thread until it completes or times out; only idle connections are free. HTTP/1.1 only; the server does not speak TLS.
- Platforms: the connection layer is tested on Linux (including under ThreadSanitizer). It is compiled for Windows with mingw-w64 and its Windows code path was exercised under Wine, which only covers the no-keep-alive fallback; macOS is untested by the author. CI on Windows and macOS is the real check.
- There is no ASGI or WSGI interface (see above), so ASGI/WSGI applications such as FastAPI or Flask cannot be mounted on it yet.
- The vendored
cpp-httplibcarries two small patches (deferred responses, and hooks for the external Asio event loop), which make the above possible; seeHTTPP_PATCHES.md. - The libcurl compatibility layer covers a subset of
curl_easy_*. - No ASGI/WSGI interface, auto-reload or multi-process workers.
Issues and pull requests are welcome.
License
Metadata
Release files for httpp 0.6.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| httpp-0.6.1.tar.gz | 287.8 kB | Without provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| httpp-0.6.1-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| httpp-0.6.1-cp39-abi3-win32.whl | CPython 3.9 | abi3 | Windows x86-32 | Details |
| httpp-0.6.1-cp39-abi3-musllinux_1_2_x86_64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ x86-64 | Details |
| httpp-0.6.1-cp39-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
| httpp-0.6.1-cp39-abi3-macosx_10_13_universal2.whl | CPython 3.9 | abi3 | macOS 10.13+ universal2 (ARM64, x86-64) | Details |
Total release size: 6.6 MB
Release files / httpp-0.6.1.tar.gz
| Download URL | httpp-0.6.1.tar.gz |
|---|---|
| Size | 287.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3b28fefda778b1e4108457c83ca39394fa71aed1782cd60ea422303ca3a71306
|
|
BLAKE2b-256 checksum How to use checksums |
ef39c1d07e0e757a70ecbaea2c495289690bb70d6353b69720237e7987d10d56
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / httpp-0.6.1-cp39-abi3-win_amd64.whl
| Download URL | httpp-0.6.1-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 803.7 kB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2f0d864bab0d50706ea91b66f96c4d3f61aa3e9955aa2594f9c47c9f778052b4
|
|
BLAKE2b-256 checksum How to use checksums |
e678155afdf203d833a3438bfcd5f1c069c63ab6f854f18cc45a41af8f5281e4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / httpp-0.6.1-cp39-abi3-win32.whl
| Download URL | httpp-0.6.1-cp39-abi3-win32.whl |
|---|---|
| Size | 715.4 kB |
| Tags | CPython 3.9 Windows x86-32 abi3 |
|
SHA-256 checksum How to use checksums |
29c1d1a5d71841bb4e4b30c5af2a6a84420cdcdc2c7e6460e474baf9ab0c8c64
|
|
BLAKE2b-256 checksum How to use checksums |
830cfc4ffbe6518049821e214ceb55a4f60f204a820ee67d39789724cea381c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / httpp-0.6.1-cp39-abi3-musllinux_1_2_x86_64.whl
| Download URL | httpp-0.6.1-cp39-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.9 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
f2b881e758289e1147781e9095664260fc3804fa6cf5f01e20a867e6b0469a13
|
|
BLAKE2b-256 checksum How to use checksums |
781e45de6966add0b41365cab5a1be72d0d68266597918d255b9b8bc9411d1d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / httpp-0.6.1-cp39-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | httpp-0.6.1-cp39-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 1.0 MB |
| Tags | CPython 3.9 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
eff4e48a2adadefc948f1bc3a9f9c29ab22a47656ab8531f89988e1deca4acea
|
|
BLAKE2b-256 checksum How to use checksums |
0d30042e58380d98f534ddc521dca8128ab038bb4fa29b925f4022481ec3b20f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / httpp-0.6.1-cp39-abi3-macosx_10_13_universal2.whl
| Download URL | httpp-0.6.1-cp39-abi3-macosx_10_13_universal2.whl |
|---|---|
| Size | 1.7 MB |
| Tags | CPython 3.9 abi3 macOS 10.13+ universal2 (ARM64, x86-64) |
|
SHA-256 checksum How to use checksums |
de1573d3f2d6965e64ec98a4c716cb37b5b7483c7d9fde529a95f93a40a36d72
|
|
BLAKE2b-256 checksum How to use checksums |
8c12a967849797c19156d53fa54c3ce05d345b0cea3572ecd45705da0d600807
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for httpp 0.6.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.