xpp::http

HTTP client and server for libxpp — Client / Server, async-first, wrapping libx's xHttpClient / xHttpServer C APIs.

Introduction

xpp::http provides a hyper/reqwest-shaped HTTP stack: a Client with get/post/put/del/patch/head conveniences, and a Server with template-injected path parameters and async handlers. Everything is Promise-based — no blocking I/O in the async path.

Header-only, C++11-compatible, with C++20 coroutine sugar (co_await / co_return) when XPP_HAS_COROUTINES is defined. Errors flow through Result<T, http::Error> — no exceptions.

Client at a Glance

C++20 — coroutines:

#include <xpp/http/client.h>

xpp::EventLoop loop;
xpp::WaitScope scope(loop);

auto client = xpp::http::Client::builder().timeout(5000).build().unwrap();

xpp::Promise<void> fetch() {
  auto r = co_await client.get("https://example.com/api");
  if (r.is_err()) { /* transport failure or 4xx/5xx */ co_return; }
  auto resp = std::move(r).unwrap();
  auto body = co_await resp.bytes();
  // ...
  co_return;
}
// Drive it — either parks the caller (running the loop) or fire-and-forget:
fetch(client).await();
xpp::spawn(fetch(client));

C++11 — .await() (parks the caller, driving the event loop):

#include <xpp/http/client.h>

xpp::EventLoop loop;
xpp::WaitScope scope(loop);

auto client = xpp::http::Client::builder().timeout(5000).build().unwrap();

auto r = client.get("https://example.com/api").await();
if (r.is_err()) { /* transport failure or 4xx/5xx */ }
auto resp = r.unwrap();
auto body = resp.bytes().await().unwrap();

The response Body is streamed through an mpsc channel with backpressure — read it via bytes(), text(), or body().read(). 4xx/5xx statuses surface as Err(Error) (a Protocol error carrying the status code).

Server at a Glance

#include <xpp/http/server.h>

xpp::EventLoop loop;
xpp::WaitScope scope(loop);

auto server = xpp::http::Server::builder()
  .route("GET /users/:id", [](xpp::http::Request req, xpp::String id) {
    return xpp::http::Response::ok(id);
  })
  .bind("127.0.0.1", 8080)   // port 0 = kernel-assigned
  .build()
  .unwrap();

auto running = server.serve();   // listens synchronously; resolves on stop()
// ... on another fiber/loop iteration:
server.stop();

Async handlers come in two flavors — the same POST /echo route:

C++20 — coroutine:

.route("POST /echo", [](xpp::http::Request req) -> xpp::Promise<xpp::http::Result<xpp::http::Response>> {
  auto body = co_await req.into_body().bytes();
  return xpp::http::Response::ok(body.unwrap());
})

C++11 — .then() chain:

.route("POST /echo", [](xpp::http::Request req) -> xpp::Promise<xpp::http::Result<xpp::http::Response>> {
  return req.into_body().bytes().then([](xpp::http::Result<xpp::Bytes> b) {
    return xpp::http::Response::ok(b.unwrap());
  });
})

Handlers run on the event loop via xpp::spawn (waker-driven, no per-request fiber). Path parameters (:name) are injected as handler arguments in pattern order. A channel-backed response body is streamed out (xHttpCtxWrite / close-delimited in HTTP/1).

Body Model

xpp::http::Body has three kinds:

KindProducerConsumer
EmptyBody::empty()EOF immediately
OnceBody::from(bytes / string / Vec)one-shot bytes
ChannelBody::from_channel(mpsc::Receiver<Bytes>)streamed, waker-driven

See Body for reading, backpressure, and streaming responses.

Error Handling

  • Transport failures (connect/DNS/timeout) → Err(Error{Kind::Connect|Dns|Timeout|…})
  • 4xx/5xx responses → Err(Error{Kind::Protocol, status})
  • Server handler returning Err → 500 to the client
  • Unmatched route → 404
  • Request body overflow (channel full) → 413