Web stack · Where a request lands

Astralis

The library for writing HTTP backends in Vala. An endpoint is a class, a route is metadata, a response is a value — and beneath it sit header and cookie handling, form parsing, server-sent events, a document builder and a layered JSON configuration system, all wired through Inversion and written with Invercargill.

Version
0.1
Licence
LGPL-3.0-or-later
Package
usm install astralis
Language
Vala

Overview

The server the stack stands on

Astralis takes an HTTP request from the socket to your code and back. It is built on GNU libmicrohttpd — driven in epoll mode from the GLib main loop, with suspend/resume so slow clients do not occupy a thread — and everything above that is Vala: routing by endpoint class, a pipeline of request values, results as values rather than writes to a socket.

The design is deliberately small. You implement the Endpoint interface, one async method that receives an HttpContext and returns an HttpResult:

class HelloEndpoint : Object, Endpoint {
    public async HttpResult handle_request(HttpContext http_context, RouteContext route) throws Error {
        return new HttpStringResult("Hello from Astralis!");
    }
}

void main() {
    var application = new WebApplication(8080);
    application.add_endpoint<HelloEndpoint>(new EndpointRoute("/hello"));
    application.run();
}
From examples/SimpleApi.vala — a complete server.

Routes can carry named segments, resolved for you into the route context:

class HelloNameEndpoint : Object, Endpoint {
    public async HttpResult handle_request(HttpContext http_context, RouteContext route_info) throws Error {
        string? name = null;
        route_info.mapped_parameters.try_get("name", out name);
        return new HttpStringResult(@"Hello, $(name ?? "Unknown")!");
    }
}

// registered as:
application.add_endpoint<HelloNameEndpoint>(new EndpointRoute("/hello/{name}"));
From examples/PathRouting.vala — the router's named-segment feature.

Endpoints are registered into the Inversion container — scoped by default, so one instance serves one request — and the stack above builds on exactly this: Statum's pages, actions and channels are Astralis endpoints, and Spry's authentication rides inside them.

Features

What it hands you

  • Endpoints and routing. Classes with route metadata; named segments like /users/{id}/posts arrive pre-parsed in the route context.
  • Headers and cookies. Request headers and cookies as enumerable collections; response headers set fluently on the result value.
  • Form data. An async FormDataParser yields fields and files with typed getters and defaults for absent fields.
  • Server-sent events. SseEndpoint subclasses broadcast to every open stream, greet new connections, and set a retry interval.
  • A document builder. MarkupDocument parses, builds and queries markup with XPath selectors — HTML without a template engine.
  • Response compression. gzip, brotli and zstd compressors with encoding negotiation; FastResource serves pre-loaded content straight from memory.
  • Layered configuration. A web-config.json system with per-environment overlays merged over the base file.

Installation

Getting the library

With USM on your machine, one command acquires, builds and installs the library together with its C header, Vala binding and introspection:

usm install astralis
Installs from the Astrologue package repository.

From source, the build lives at the repository root:

git clone <the astralis repository>
cd astralis
meson setup . build
ninja -C build
ninja -C build install
The same meson invocation the package's own build script uses.

Then compile against it with Vala's usual package mechanism:

valac --pkg astralis-0.1 my-server.vala
The installed astralis-0.1.deps resolves the Invercargill and Inversion bindings for you.

Usage & documentation

Working with requests

Astralis has no README; its documentation is its examples. Seventeen runnable programs cover the API surface, and each idiom below is lifted from them.

Responses are values

An HttpResult carries body, status and headers together, so handlers stay pure and testable — headers chain onto the constructor:

return new HttpStringResult(user.to_json())
    .set_header("Content-Type", "application/json")
    .set_header("Access-Control-Allow-Origin", "*");
From examples/JsonApi.vala and PathRouting.vala.

Failures use the same vocabulary — a helper returns the status you name:

return error_response(@"Resource with ID $id not found", StatusCode.NOT_FOUND);
From examples/ErrorHandling.vala.

Headers, cookies and queries

Request headers and cookies arrive as Invercargill collections, so the whole Enumerable toolkit applies — count, iterate, filter — with single-field getters alongside:

var value = http_context.request.get_header(header_name);

http_context.request.cookies.to_immutable_buffer()
    .iterate(kv => parts.add(@"$(kv.key): $(kv.value)"));

if (http_context.request.cookies.to_immutable_buffer().count() == 0) {
    parts.add("(No cookies sent)");
}
From examples/HeadersAndCookies.vala.

Form bodies parse asynchronously, with getters that take defaults for absent fields:

FormData form_data = yield FormDataParser.parse(
    http_context.request, upload_path);

var name    = form_data.get_field_or_default("name", "Anonymous");
var email   = form_data.get_field_or_default("email", "no-email@example.com");
var country = form_data.get_field_or_default("country", "us");
From examples/FormData.vala — fields and uploaded files arrive together.

Server-sent events

SseEndpoint is the base class for push. A singleton instance holds shared state; new_connection greets each client and broadcast_event reaches every open stream:

public class CounterEndpoint : SseEndpoint {
    private int counter = 0;
    private Mutex counter_mutex = Mutex();

    public override uint retry_interval { get { return 2000; } }

    public override async void new_connection(HttpContext http_context,
            RouteContext route_context, SseStream stream) {
        yield stream.send_event(
            new SseEvent.with_type("counter", counter.to_string()));
    }

    public async void increment() {
        counter++;
        yield broadcast_event(
            new SseEvent.with_type("counter", counter.to_string()));
    }
}
From examples/SseExample.vala — a mutex-guarded counter broadcast to all tabs.

Building documents

When a handler must produce HTML rather than JSON, the markup subsystem parses, edits and queries documents without string templates:

var doc = new MarkupDocument.from_string(page_html);

var header_card = body.append_child_element("div");
header_card.append_child_with_text("h1", "A heading");
var toggle_form = item_div.append_child_element("form");
var toggle_btn = toggle_form.append_child_element("button");
From examples/DocumentBuilder.vala — elements are built by appending children; XPath selectors find them again.

MarkupTemplate composes documents, and XPath results come back as lazy Enumerables over the tree.

Static content and compression

FastResource holds content pre-loaded in memory — the example notes it should be registered as a startup or singleton since it keeps its bytes for the process lifetime:

FastResource.from_string(content)   // load from a string
FastResource.from_byte_array(bytes) // load from bytes
FastResource(path)                  // load from the filesystem
From examples/FastResources.vala — the three constructors.

FilesystemResource serves a directory tree, and the compressor components negotiate gzip, brotli and zstd against what the client accepts — the file-server example announces exactly that:

Available compression: gzip, brotli, zstd

Try:
  curl http://localhost:$port/
  curl --compressed http://localhost:$port/style.css
From examples/FileServer.vala, a complete static server in 141 lines.

Configuration

Settings live in a web-config.json at the application root, and an environment overlay — web-config.staging.json, selected with ASTRALIS_ENV=staging — merges over it. Inside a handler, the configuration is injected through Inversion and read with typed getters:

var config = inject<WebConfig>();

var port     = config.get_int("port", 8080);
var app_name = config.get_string("app_name", "Unknown App");
var debug    = config.get_bool("debug", false);
From examples/WebConfigExample.vala — every getter carries a default.

Inside the box

The source tree names its own layers: Core holds WebApplication, the pipeline, HttpContext and the WebConfig family; Server holds the libmicrohttpd integration and socket monitor; Components holds routing and the three compressors; Endpoints holds the endpoint contracts and resource servers; Data holds form parsing; and Markup holds the document builder. The examples directory doubles as a guided tour of each.

Package & source

The ledger

Version
0.1 (from MANIFEST.usm)
Licence
LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.
Package
usm install astralis
Provides
libastralis-0.1.so, astralis-0.1.h, astralis-0.1.vapi + .deps, astralis-0.1.gir, astralis-0.1.typelib, astralis-0.1.pc
Needs at runtime
GLib and GObject, libmicrohttpd, json-glib, libxml2, zlib, brotli and zstd encoders, plus the stack beneath: libinvercargill-1.so and libinversion-0.1.so.
Source
The repository Tilo15/astralis on the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.

The typelib ships as astralis-0.1.typelib, matching the GIR's namespace — an earlier package installed it under a lib-prefixed name that introspection consumers could never resolve; the mismatch is fixed and the manifest now names the file the build actually produces.