--- title: 'Hook-Referenz: Netze und Nachrichten' description: 'http, smtp, channel und ratelimit im Hook-JS: Signaturen aus internal/hook, je ein Beispiel und je ein belegter Fehlerfall.' --- In Hooks sprichst du diese vier Pakete über `context` an, z. B. für einen Fetch-Request oder den Mail-Versand. Die Signaturen stammen aus den `// args:`- bzw. `// JS signature:`-Kommentaren in `internal/hook/context_*.go` im tibi-server; alle vier Pakete sind dort lückenlos kommentiert, keine Signatur musste abgeleitet werden. Jede Funktion zeigt genau einen Fehlerfall mit dem originalen Error-Text aus Code oder Tests. ## http – Externe Requests absetzen Quelle: `context_http.go`, registriert per `RegisterPackageFunctions("http", ...)` mit `fetch`, `fetchStream`. Beide teilen sich die Argumentprüfung in `parseHTTPArgs`; `fetch` liest den Body vollständig ein (Timeout-Default `10000` ms), `fetchStream` liefert einen zeilenweisen Reader für SSE und Streaming (Timeout-Default `0`, also kein Timeout). ### http.fetch **Signatur:** `http.fetch(url: string, options?: { method?: string, headers?: object, body?: string, timeout?: number }): object` — aus `// args: url:string, options:map[string]interface{}` (`context_http.go:124`). Antwortet mit `{ headers, status, statusText, trailer, url, body: { text(), json() } }`; `body.json()` wirft bei ungültigem JSON den Parser-Error als JS-Exception. ```js title="http.fetch – Beispiel" ;(function () { const res = context.http.fetch("https://api.example.com/content", { method: "GET", headers: { Accept: "application/json" }, timeout: 5000 }) throw { status: 200, json: { status: res.status, data: res.body.json() } } })() ``` **Fehlerfall (Error):** Ohne URL brichst du mit `missing url in http.fetch function` ab (`context_http.go:37`); eine URL vom falschen Typ liefert `url must be string in http.fetch function` (`context_http.go:45`), belegt durch die Test-Assertions auf beide Texte (`context_http_test.go:432`, `context_http_test.go:442`). ### http.fetchStream **Signatur:** `http.fetchStream(url: string, options?: { method?: string, headers?: object, body?: string, timeout?: number }): object` — aus `// args: url:string, options:map[string]interface{}` (`context_http.go:186`). Antwortet mit `{ headers, status, statusText, body: { read(), close() } }`; `read()` liefert die nächste Zeile als String oder `null` bei EOF und schließt dann automatisch. ```js title="http.fetchStream – Beispiel" ;(function () { const res = context.http.fetchStream("https://api.example.com/events", { headers: { Accept: "text/event-stream" } }) const first = res.body.read() res.body.close() throw { status: 200, json: { status: res.status, first: first } } })() ``` **Fehlerfall (Error):** Ohne URL brichst du mit `missing url in http.fetchStream function` ab (`context_http.go:37`); `options` als String statt Objekt scheitert mit `options must be object in http.fetchStream function` (`context_http.go:52`), belegt durch die Test-Assertion auf diesen Text (`context_http_test.go:452`). ## smtp – Mails versenden Quelle: `context_smtp.go`, registriert per `RegisterPackageFunctions("smtp", ...)` mit `sendMail`. Der Versand nutzt den Server-Mail-Host aus der Config oder einen per `host`, `username` und `password` übergebenen Host. Beachte: Die Error-Texte nennen wörtlich `mail.send`, obwohl das Paket in JS `smtp` heißt — Altlast-Namensrest im Server-Code, kein Alias. ### smtp.sendMail **Signatur:** `smtp.sendMail(options: { to: string | Array, cc?: string | Array, bcc?: string | Array, from: string, fromName?: string, replyTo?: string, subject?: string, plain?: string, html?: string, attach?: string | Array, host?: string, username?: string, password?: string }): void` — aus `// args: options:map[string]interface{}` (`context_smtp.go:23`). Gibt bei Erfolg `null` zurück; Anhänge sind Pfade im Config-Verzeichnis. ```js title="smtp.sendMail – Beispiel" ;(function () { context.smtp.sendMail({ to: "redaktion@example.com", from: "noreply@example.com", fromName: "Tibi CMS", subject: "Neuer Beitrag", plain: "Ein neuer Beitrag wartet auf Freigabe." }) throw { status: 200, json: { sent: true } } })() ``` **Fehlerfall (Error):** Ohne `options` scheitert der Aufruf mit `missing options in mail.send function` (`context_smtp.go:37`); ohne Empfänger meldet der Code `missing or invalid to in options of mail.send function` (`context_smtp.go:76`), ohne Absender `missing or invalid from in options of mail.send function` (`context_smtp.go:117`) — beides belegt in `context_smtp_test.go:77` („error - missing options") und `context_smtp_test.go:88` („error - missing from"). ## channel – Realtime-Nachrichten Quelle: `context_channel.go`, registriert per `RegisterPackageFunctions("channel", ...)` mit `subscribe`, `send`. `subscribe` blockiert per `receive()`, bis eine Nachricht eintrifft, der Client trennt (`null`) oder die Subscription geschlossen wird (Exception mit Code `channel_closed`); der Hook-Timeout wird dafür deaktiviert. ### channel.subscribe **Signatur:** `channel.subscribe(name: string, options?: { bufferSize?: number, onFull?: string, messageTTL?: number, lastN?: number, maxAge?: number }): { receive(), close() }` — aus `// JS signature: channel.subscribe(name: string, options?: object): Subscription` (`context_channel.go:21`). ```js title="channel.subscribe – Beispiel" ;(function () { const sub = context.channel.subscribe("news", { lastN: 10 }) const msg = sub.receive() sub.close() throw { status: 200, json: { message: msg } } })() ``` **Fehlerfall (Error):** Außerhalb von Hooks (in Jobs) scheitert der Aufruf mit `channel.subscribe is only available in hooks, not in jobs` (`context_channel.go:29`); ohne Kanalnamen meldet der Code `channel.subscribe requires a channel name` (`context_channel.go:38`). ### channel.send **Signatur:** `channel.send(name: string, data: any): void` — aus `// JS signature: channel.send(name: string, data: any): void` (`context_channel.go:109`). Publiziert an alle Subscriber des Kanals. ```js title="channel.send – Beispiel" ;(function () { context.channel.send("news", { title: "Eilmeldung", active: true }) throw { status: 200, json: { sent: true } } })() ``` **Fehlerfall (Error):** Ohne Kanalnamen und Daten scheitert der Aufruf mit `channel.send requires a channel name and data` (`context_channel.go:117`); ein leerer Kanalname liefert `channel.send: channel name must be a non-empty string` (`context_channel.go:121`). ## ratelimit – Backoff und Sliding Window Quelle: `context_ratelimit.go`, registriert per `RegisterPackageFunctions("ratelimit", ...)` mit `backoffCheck`, `backoffRecord`, `backoffReset`, `windowCheck`, `windowReset`, `windowCount`. Alle sechs Funktionen brauchen denselben `key` (nicht-leerer String) und einen verfügbaren Limiter. ### ratelimit.backoffCheck **Signatur:** `ratelimit.backoffCheck(key: string): { blocked: boolean, retryAfterMs: number }` — aus `// JS signature: ratelimit.backoffCheck(key: string): { blocked: boolean, retryAfterMs: number }` (`context_ratelimit.go:41`). ```js title="ratelimit.backoffCheck – Beispiel" ;(function () { const state = context.ratelimit.backoffCheck("login:mailer@example.com") if (state.blocked) { throw { status: 429, json: { retryAfterMs: state.retryAfterMs } } } throw { status: 200, json: { blocked: false } } })() ``` **Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`); ohne verfügbaren Limiter meldet der Code `ratelimit: limiter not available` (`context_ratelimit.go:23`). ### ratelimit.backoffRecord **Signatur:** `ratelimit.backoffRecord(key: string): void` — aus `// JS signature: ratelimit.backoffRecord(key: string): void` (`context_ratelimit.go:59`). Zeichnet einen Fehlversuch auf und verschärft das exponentielle Backoff. ```js title="ratelimit.backoffRecord – Beispiel" ;(function () { context.ratelimit.backoffRecord("login:mailer@example.com") throw { status: 200, json: { recorded: true } } })() ``` **Fehlerfall (Error):** Ein leerer Schlüssel scheitert mit `ratelimit: key must be a non-empty string` (`context_ratelimit.go:34`). ### ratelimit.backoffReset **Signatur:** `ratelimit.backoffReset(key: string): void` — aus `// JS signature: ratelimit.backoffReset(key: string): void` (`context_ratelimit.go:74`). Setzt den Backoff-Zähler zurück, z. B. nach erfolgreichem Login. ```js title="ratelimit.backoffReset – Beispiel" ;(function () { context.ratelimit.backoffReset("login:mailer@example.com") throw { status: 200, json: { reset: true } } })() ``` **Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`). ### ratelimit.windowCheck **Signatur:** `ratelimit.windowCheck(key: string): { allowed: boolean, retryAfterMs: number }` — aus `// JS signature: ratelimit.windowCheck(key: string): { allowed: boolean, retryAfterMs: number }` (`context_ratelimit.go:89`). Zählt den Aufruf als Hit und prüft das Sliding Window in einem Schritt. ```js title="ratelimit.windowCheck – Beispiel" ;(function () { const check = context.ratelimit.windowCheck("api:mailer@example.com") if (!check.allowed) { throw { status: 429, json: { retryAfterMs: check.retryAfterMs } } } throw { status: 200, json: { allowed: true } } })() ``` **Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`); ohne verfügbaren Limiter meldet der Code `ratelimit: limiter not available` (`context_ratelimit.go:23`). ### ratelimit.windowReset **Signatur:** `ratelimit.windowReset(key: string): void` — aus `// JS signature: ratelimit.windowReset(key: string): void` (`context_ratelimit.go:107`). Setzt das Sliding Window für den Schlüssel zurück. ```js title="ratelimit.windowReset – Beispiel" ;(function () { context.ratelimit.windowReset("api:mailer@example.com") throw { status: 200, json: { reset: true } } })() ``` **Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`). ### ratelimit.windowCount **Signatur:** `ratelimit.windowCount(key: string): number` — aus `// JS signature: ratelimit.windowCount(key: string): number` (`context_ratelimit.go:122`). Liefert die aktuelle Trefferzahl im Fenster, ohne zu zählen. ```js title="ratelimit.windowCount – Beispiel" ;(function () { const n = context.ratelimit.windowCount("api:mailer@example.com") throw { status: 200, json: { hits: n } } })() ``` **Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`). Weiter: [Hook-Kontexte](/reference/hook-kontexte/) als Paket-Index, [Hooks schreiben](/projekt/hooks-schreiben/) für Lifecycle und Starter-Muster. Verifiziert am 68b4278 2026-10-16 (tibi-server)