HTTP im Überblick
Das HTTP-Modul ist getrennt von den I/O-Broker-Actors — HTTP-Server passen nicht in die Form “eine Verbindung, viele Nachrichten”, deshalb bekommen sie eine eigene DSL und Backend- Abstraktion.
import { ActorSystem } from 'actor-ts';import { path, get, post, completeJson, entity, concat } from 'actor-ts/http';
const system = ActorSystem.create('my-app');
const routes = path('api', path('orders', concat( get(async () => completeJson(200, { orders: [] })), post(async (req) => { const order = entity<NewOrder>(req); // ... behandeln ... return completeJson(201, { id: 'o-1' }); }), ), ),);
const binding = await system.http(8080).bind(routes);console.log(`gebunden auf ${binding.host}:${binding.port}`);Drei Dinge passieren hier:
- Die Route-DSL —
path,get,post,concatkomponieren einen Routenbaum. Typsicher; wird beim Bind zu einer flachen Liste kompiliert. - Der Marshaller —
entity<T>(req)dekodiert den Request- Body je nach Content-Type;completeJson/completeTextencoden die Response. - Der Shortcut —
system.http(port).bind(routes)startet einen Server mit dem Standard-Fastify-Backend. Für ein anderes Backend übergibst Dubackend:(siehe unten).
Die drei Bausteine
Abschnitt betitelt „Die drei Bausteine“| Baustein | Lebt in | Seite |
|---|---|---|
| Routing | actor-ts/http exportiert path, get, post, complete* usw. | Route-DSL |
| Marshalling | entity<T>(req)-Dekodierung + completeJson-Kodierung. | Marshalling |
| Backend | Pluggable HTTP-Server — Fastify per Default, Hono, Express. | Backends |
Die DSL baut einen In-Memory-Routenbaum;
system.http(...).bind(routes) flacht ihn ab und registriert
alles beim gewählten Backend.
Die Backends
Abschnitt betitelt „Die Backends“import { HonoBackend } from 'actor-ts/http';
// Default — keine Einrichtung nötig; Fastify ist eine Hard-Dependency:await system.http(8080).bind(routes);
// Explizite Wahl (Express / Hono sind Peer-Deps; Opt-in):await system.http(8080, { backend: new HonoBackend() }).bind(routes);
// Volle Kontrolle — die Extension-Surface bleibt erhalten:import { HttpExtensionId } from 'actor-ts/http';await system.extension(HttpExtensionId) .newServerAt('0.0.0.0', 8080) .useBackend(new HonoBackend()) .bind(routes);| Backend | Runtime-Eignung | Wann |
|---|---|---|
FastifyBackend (Default) | Bun, Node | Produktions-Default — Fastify ist das am stärksten kampferprobte. |
ExpressBackend | Node, Bun | Das bestehende Express-Middleware-Ökosystem passt sauber rein. |
HonoBackend | Bun, Node, Deno | Schlank + edge-tauglich; die Bun- und Deno-nativeste Option. |
Alle drei implementieren dasselbe HttpServerBackend-Interface —
die Route-DSL kompiliert identisch; das Backend besitzt nur die
Listen-/Dispatch-Schleife.
Siehe die Backend-Seiten für die Konfigurationsoptionen, die jedes akzeptiert.
Der HTTP-Client
Abschnitt betitelt „Der HTTP-Client“const response = await http.singleRequest({ method: 'POST', url: 'https://api.example.com/orders', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sku: 'book-1' }),});
console.log(response.status, response.body);Der gemeinsame Client umschließt das native fetch der Runtime.
Funktioniert identisch auf Bun, Node und Deno (jede unterstützte
Runtime hat fetch eingebaut).
Schranken, die jeder Aufruf mitbringt
Abschnitt betitelt „Schranken, die jeder Aufruf mitbringt“Eine Antwort wird im Speicher gepuffert, bevor du sie siehst — deshalb sind die drei Dinge, die die Gegenstelle kontrolliert, standardmäßig begrenzt:
| Schranke | Default | Override pro Request |
|---|---|---|
| Deadline | 30 s | timeoutMs — 0 nimmt einen Aufruf ganz heraus |
| Response-Body | 8 MiB | maxResponseBytes |
| Redirect-Hops | 5, gefolgt | redirect, maxRedirects |
Jenseits der Obergrenze wird der Request abgebrochen, während
der Body noch eintrifft, und der Aufruf wirft
HttpResponseTooLargeError — eine übergroße oder endlose Antwort
alloziert also nie zu Ende. Jenseits der Deadline bricht er
genauso ab wie bisher schon bei explizitem timeoutMs; neu ist
nur, dass ein Aufruf ohne genannte Deadline jetzt eine hat.
Jeder Override wird beim Aufruf geprüft, nicht nur beim Bau des
Clients. Jede Schranke wird über einen Vergleich durchgesetzt, und
ein NaN verliert jeden Vergleich lautlos: Ein timeoutMs von
NaN scharft keinen Timer, ein maxResponseBytes von Infinity
löst nie aus, und beides meldet nichts — die Schranke hört einfach
auf zu existieren. Ein Wert außerhalb des Wertebereichs wirft
deshalb OptionsError, bevor ein Socket geöffnet wird.
timeoutMs: 0 bleibt der eine legitime Weg zu keiner Deadline und
maxRedirects: 0 der Weg, den ersten Redirect abzulehnen.
Hebe sie pro Aufruf an, wenn ein einzelner Endpunkt die Ausnahme ist:
const report = await system.extension(HttpExtensionId).client.get( 'https://api.example.com/exports/daily', { maxResponseBytes: 64 * 1024 * 1024, timeoutMs: 120_000 },);Oder gib einer ganzen Integration ihren eigenen Client, statt den zu lockern, den sich alle Aktoren im System teilen:
import { HttpClientOptions } from 'actor-ts/http';
const exportClientOptions = HttpClientOptions.create() .withMaxResponseBytes(64 * 1024 * 1024) .withDefaultTimeoutMs(120_000);
const exportClient = system.extension(HttpExtensionId).newClient(exportClientOptions);Oder verschiebe die Zahlen, die das ganze Deployment erbt, ohne Code
anzufassen — unter actor-ts.http.client:
actor-ts.http.client { maxResponseBytes = 32M defaultTimeoutMs = 60s}Die Reihenfolge ist die übliche: Ein Request schlägt den Client, auf
dem er gemacht wurde, der schlägt HOCON, das schlägt die eingebauten
Defaults. Der gemeinsame HttpExtensionId-Client liest diesen Block
ebenfalls — den Boden anzuheben heißt also nicht, ihn aufzugeben. Ein
direkt gebautes new HttpClient() hat kein System, aus dem es Config
lesen könnte, und behält die eingebauten Zahlen.
Wohin ein Redirect dich bringen darf
Abschnitt betitelt „Wohin ein Redirect dich bringen darf“Der Client folgt Redirects selbst, statt sie der Plattform zu überlassen — so fallen die Entscheidungen zwischen den Hops:
redirect | Verhalten bei einem 3xx mit Location |
|---|---|
'follow' (Default) | Folgen, bis zu maxRedirects Hops |
'error' | HttpRedirectError; das Ziel wird nie kontaktiert |
'manual' | Den 3xx unverändert zurückgeben, Location in headers lesbar |
Gefolgt wird höchstens 5 Hops weit (die Plattform erlaubt 20), und ein gefolgter Hop ist kein blindes Wiederholen des ursprünglichen Requests:
- Ein Hop, der die Origin wechselt, verliert
authorization,cookieundproxy-authorization— ein Redirect kann ein Bearer-Token also nicht zu einem Host tragen, den du nie ausgewählt hast. - Ein
303— und ein301/302nach einemPOST— läuft alsGETweiter, ohne Body und ohne dessencontent-type, genau wie die Fetch-Spezifikation es vorschreibt; ein Schreibzugriff wird so nie gegen einen zweiten Host wiederholt. - Ein Ziel, das nicht
http:oderhttps:ist, wird rundheraus abgelehnt. - Deadline und Byte-Obergrenze gelten kumulativ über die ganze Kette, nicht pro Hop; Bodies von Zwischen-3xx werden verworfen.
Abgelehnt wird, bevor der Hop überhaupt abgesetzt wird — darum geht es: Eine Prüfung, wo eine Kette gelandet ist, hat jeden Request darin bereits gestellt, und genau das Absetzen des Requests ist der Seiteneffekt, den es zu verhindern gilt.
const webhookClientOptions = HttpClientOptions.create() .withRedirect('error') .withMaxResponseBytes(256 * 1024);
const webhookClient = system.extension(HttpExtensionId).newClient(webhookClientOptions);response.url nennt den Hop, der tatsächlich geantwortet hat —
darauf prüfst du, wenn es darauf ankommt, welcher Host die Bytes
geliefert hat.
Für aufwendigere Client-Muster (Retries, Caching) wickle den Client in einen Retry- + Circuit-Breaker-Aufruf.
Middleware
Abschnitt betitelt „Middleware“import { cached, get, path, rateLimit } from 'actor-ts/http';import { InMemoryCache } from 'actor-ts/cache';
// One cache per middleware — never one shared instance (see below).const limiterCache = new InMemoryCache();const responseCache = new InMemoryCache();
const limited = rateLimit({ cache: limiterCache, windowMs: 1_000, max: 100, key: (request) => request.remoteAddress ?? 'unknown',});
const readThrough = cached({ cache: responseCache, ttlMs: 30_000, key: (request) => request.path,});
const routes = path('api', get(limited(readThrough(apiHandler))));Das Framework liefert drei:
| Middleware | Was sie tut |
|---|---|
Response-Cache (cached) | Cached die Antwort eines Handlers, key’d über eine key(req)-Funktion. |
Rate-Limit (rateLimit) | Per-Key-Request-Limiter mit Fixed-Window-Counter im Cache. |
Idempotency-Key (idempotent) | Dedupliziert Writes anhand eines Idempotency-Key-Headers. |
Jede ist ein Handler-Wrapper — du rufst sie mit den Optionen auf und übergibst der zurückgegebenen Funktion den Handler, der geschützt werden soll. Kombiniere durch Verschachteln auf Handler-Ebene. Siehe die Middleware-Seiten.
HTTP an Actors anschließen
Abschnitt betitelt „HTTP an Actors anschließen“Der Routen-Handler gibt einen Promise<HttpResponse> zurück —
darin kannst du frei Actor-Calls awaiten:
const routes = path('orders', path(':id', get(async (req) => { const id = req.path.split('/').pop(); const order = await orderRegistry.ask({ kind: 'get', id }, 5_000,); return completeJson(200, order); }), ),);Das ist das häufige Muster — HTTP-Handler sind ein dünner Adapter: Request parsen, einen Actor fragen, die Antwort marshallen. Halte Business-Logik in Actors; HTTP-Handler bleiben kurz.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Route-DSL — die volle DSL:
path,get/post/put/usw.,concat,complete*,redirect,reject. - Marshalling —
entity<T>, Content-Type-gesteuerte Dekodierung, Response-Kodierung. - Fastify-Backend — Konfiguration des Default-Backends.
- Hono-Backend / Express-Backend — Alternativen.
- Response-Cache-Middleware — Caching für GET-Responses.
- Rate-Limit-Middleware — Rate-Limiting pro Key.
- Idempotency-Key-Middleware — Write-Deduplizierung für Mutationen.
Die HttpExtension-API-Referenz
deckt die volle Oberfläche ab.
