IR
irwinrodriguez.dev
Zurück zur Dokumentation

Erweiterte Funktionen

Was über dem Ausliefern eines JSON liegt: Echtzeitereignisse, signierte ausgehende Webhooks, der Status-Endpunkt, der Dienstmodus, die Verteilung in die Installation und der Prüfstand.

Server-Sent Events (SSE)

SSE lässt den Server Ereignisse über eine offen bleibende HTTP-Verbindung an den Client schieben. Der Vertrag hat drei Teile, und alle drei sind vom Controller aus sichtbar: die Route muss auf /events enden und deklariert sein, der Client muss Accept: text/event-stream senden oder bekommt 406, und das Backend erklärt mit res.events(), welche Ereignisse es unterstützt. Framing, retry:, das Ereignis connected, die Pings und das Nachliefern über Last-Event-Id übernimmt die Middleware.

In JSON aktivieren:

"middleware": { "sse": { "enabled": true } }

Im Controller:

SSE: Bestellereignisse deklarieren und senden
* La ruta TIENE que terminar en /events. Aqui no se escribe el
* framing a mano ni se hace un bucle: eso ocuparia uno de los pocos
* hilos VFP del pool durante todo el rato. Lo unico que hace este
* endpoint es DECLARAR que eventos existen.
PROCEDURE pSuscripcion(req, res) HELP "GET: pedidos/events public"
    res.events("pedido.creado")
    res.events("pedido.enviado")
    RETURN THIS.Ok(.NULL.)
ENDPROC

* Y se emite desde cualquier otro endpoint, cuando pasa algo.
PROCEDURE pCrearPedido(req, res) HELP "POST: pedidos"
    LOCAL lcId, loRes

    lcId = THIS.CrearPedido(req.json)

    res.emit("pedido.creado", '{"id":"' + lcId + '"}')

    loRes    = THIS.newObject("id")
    loRes.id = lcId
    RETURN THIS.Created(loRes, "/pedidos/" + lcId)
ENDPROC

JavaScript-Client:

// EventSource manda Accept: text/event-stream por su cuenta.
// Sin esa cabecera, el middleware responde 406.
const es = new EventSource('/pedidos/events?events=pedido.creado');

// El middleware manda primero un evento 'connected' que dice que
// eventos hay, y despues los tuyos, cada uno con su id.
es.addEventListener('pedido.creado', (e) => {
  console.log(JSON.parse(e.data).id);
});

// Si se corta, el navegador reconecta solo mandando Last-Event-Id,
// y el servidor repone lo que tenga en cache: la entrega es
// at-least-once, asi que deduplica por e.lastEventId.
es.onerror = () => es.close();
Ein Ereignis anzufordern, das das Backend nicht deklariert, abonniert NICHTS Nicht alles. Der Platzhalter ist nur die Vorgabe, wenn Sie gar kein ?events= senden.

Webhook-System

FoxServer setzt Standard Webhooks in beide Richtungen um, geprüft und mit Tests. Das Wesentliche am Modell: ausgehende Webhooks werden nicht aus dem Code ausgelöst, sondern im Manifest deklariert, und beim Bedienen dieses Endpunkts geht der Rumpf der Antwort signiert an die aktiven Abonnenten.

  • Deklariert mit foxserver webhook-add-out, eingestellt mit webhook-set --retries --timeout --backoff
  • HMAC-SHA256-Signatur in base64 über {id}.{timestamp}.{body}, mit Präfix v1,
  • Das Geheimnis darf whsec_<base64> sein (das Format von svix, Clerk oder Resend); dann sind die dekodierten Bytes der Schlüssel
  • retries: N bedeutet N Versuche insgesamt, nicht N zusätzliche. Und ein 404 des Abonnenten bricht die Wiederholungen sofort ab
  • Was aufgebraucht ist, landet in <host>\queue\webhooks\dead-letters\
  • Alle Versuche teilen dieselbe webhook-id, damit der Empfänger entdoppeln kann, und jeder wird mit eigenem Zeitstempel neu signiert
  • Getestet wird ohne die Konsole zu verlassen, mit foxserver webhook-test

Einen ausgehenden deklarieren:

REM Se declara con la CLI, no editando el manifiesto a mano.
foxserver webhook-add-out --project C:\proyectos\tienda ^
    --route pedidos --method POST ^
    --url https://socio.com/webhooks --secret whsec_...

REM Y se ajusta despues:
foxserver webhook-set --project C:\proyectos\tienda ^
    --route pedidos --retries 3 --timeout 30 --backoff 5,15,30

REM Para probarlo sin salir de la consola:
foxserver webhook-test --project C:\proyectos\tienda --route pedidos

Und das war es: der Endpunkt wird wie jeder andere geschrieben. Was er zurückgibt, wird gesendet.

pCrearPedido: was es zurückgibt, geht signiert hinaus
* Ni una linea de webhook aqui dentro. Lo que devuelve este endpoint
* es exactamente lo que sale firmado hacia los suscriptores.
PROCEDURE pCrearPedido(req, res) HELP "POST: pedidos"
    LOCAL lcId, loRes

    lcId = THIS.CrearPedido(req.json)

    loRes          = THIS.newObject("orderId,status")
    loRes.orderId  = lcId
    loRes.status   = "created"
    RETURN THIS.Created(loRes, "/pedidos/" + lcId)
ENDPROC

Was beim Abonnenten ankommt:

POST https://socio.com/webhooks
webhook-id: 550e8400-e29b-41d4-a716-446655440000
webhook-timestamp: 1789476225
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

{"orderId":"ORD-001","status":"created"}

Der webhook-timestamp ist Epoch in Sekunden, und die Signatur ist v1, plus das base64 des HMAC-SHA256 über {id}.{timestamp}.{body}. Läuft Ihr Controller bei einem EINGEHENDEN Webhook überhaupt an, hat die Middleware Signatur und das Fünf-Minuten-Fenster gegen Wiedereinspielung schon geprüft: der Webhook ist bereits echt. Eine Sache ist zu beachten: ein Anbieter mit eigener Signatur, etwa Stripe oder GitHub, wird nicht als eingehend deklariert, denn die Middleware würde ihn ablehnen, weil er keine Standard Webhooks sind. Diese Route wird als normaler Endpunkt bedient, und Sie prüfen sie selbst.

Neu laden ohne Neustart

Es gibt kein automatisches Neuladen, und das gehört deutlich gesagt, weil das Wort „Hot Reload“ etwas anderes nahelegt: FoxServer überwacht die Festplatte nicht. Es gibt einen Schalter, und der genügt: nach einem foxserver build gibt ein POST an den Status-Endpunkt die COM-Objekte des Servers frei, und die nächste Anfrage erzeugt sie sauber neu, schon mit Ihrer neuen DLL.

Der vollständige Ablauf:

  1. Sie bearbeiten und speichern Ihre .prg.
  2. Sie kompilieren mit foxserver build (oder aus FoxForge) neu, und die MTDLL des Projekts entsteht neu. Achtung: dafür braucht es eine erhöhte Konsole, denn BUILD MTDLL registriert am Ende die Coclass.
  3. POST <Status-Endpunkt>/reload mit dem Header X-Status-Pin.
  4. Der Server gibt seine COM-Referenzen frei. Die nächste Anfrage erzeugt die Controller erneut, nun mit dem neuen Code.

Der Aufruf:

curl -X POST http://127.0.0.1:8099/status/reload ^
     -H "X-Status-Pin: <tu PIN>"

Der Status-Endpunkt

Er wird im Manifest deklariert und verlangt eine PIN: ohne den Header X-Status-Pin oder mit falscher PIN antwortet er mit 401; hat der Server keine PIN eingerichtet, mit 403. Jedes andere Verb als GET auf dieser Route ergibt 404, nicht 405.

"server": {
  "checkstatusenabled": true,
  "endpoint": "status",
  "statuspin": "<cifrado con middleware-set>"
}
curl http://127.0.0.1:8099/status -H "X-Status-Pin: <tu PIN>"

Und was er zurückgibt:

{
  "uptime": {
    "raw": "...",
    "formatted": "5d 3h 22m"
  },
  "memory": {
    "raw": 257425408,
    "formatted": "245 MB"
  },
  "connections": {
    "current": 7
  },
  "config": {
    "host": "127.0.0.1",
    "port": 8099,
    "middleware": {
      "auth": true,
      "logging": true
    }
  },
  "license": {
    "kind": "Runtime",
    "holder": "Tu Empresa, S.L.",
    "seats": 1,
    "isMaintenanceActive": true,
    "limitedByMaintenance": false
  },
  "metrics": {
    "successRate": 99.97,
    "rejectedRequests": 0,
    "rejectionRate": 0
  }
}
license.kind entscheidet, wer Sie aufrufen darf Developer oder Runtime. Bei Developer antwortet der Server auf jede Anfrage, die nicht von 127.0.0.1 kommt, mit 403. Das ist das Erste, was man prüft, wenn von einem anderen Rechner alles 403 ergibt.

Windows-Dienstmodus

FoxServer kann als Windows-Dienst installiert werden für automatischen Start und Hintergrundbetrieb ohne Konsolenfenster.

Befehle:

# Install as service
bin\FoxServer.Host.exe --install-service

# Uninstall service
bin\FoxServer.Host.exe --uninstall-service

# Manage via Windows Services
services.msc
Im Dienstmodus gehen die Protokolle in die gewohnten Dateien. Die Windows-Ereignisanzeige erhält sie NICHT: dort wird nur dann ein Eintrag geschrieben, wenn das Schreiben des Protokolls selbst scheitert. Den Dienst meldet der Installer an; von Hand mit bin\FoxServer.Host.exe --install-service aus einer erhöhten Konsole.

In die Installation ausrollen

foxserver serve startet den Host mit Blick auf den Ordner build\ Ihres Projekts: zum Testen gut, aber verify.cmd sieht diesen Server nicht und der Windows-Dienst startet ihn nicht, denn beide schauen in config\servers\ der Installationswurzel. Wenn er Teil davon werden soll:

foxserver deploy --project C:\proyectos\almacen
Desplegado Almacen en la instalacion:
  C:\Program Files\FoxServer\config\servers\ALMACEN.json
  clave: 127.0.0.1:8099:
Vor dem Schreiben wird geprüft, dass kein anderes Manifest denselben Schlüssel Host:Port:Präfix hat. Gibt es eines, endet es mit 11 und nennt es - und das zu Recht: der Host wurde eines starten und das andere mit einer nur im Log sichtbaren Warnung überspringen, einer der unangenehmsten Fehler überhaupt. --force überspringt die Prüfung.

Die Umgebung Ihrer Anwendung wiederherstellen

Ihre Geschäftslogik braucht das, was die main.prg Ihres ERP aufbaut - Konfiguration, Verbindung, Stammtabellen -, und ein Controller wird ohne all das geboren. Die Logik muss nicht neu geschrieben werden: die Umgebung muss einmal wiederhergestellt werden. Dafür gibt es den Lifecycle.

MiErpLifecycle: die ERP-Umgebung beim Start aufbauen
define class MiErpLifecycle as ApplicationLifecycle olepublic

    procedure OnApplicationStarting
        local loCfg
        * Lo que hace el main.prg de tu aplicacion: SET PATH, abrir las
        * maestras, leer la configuracion, conectar.
        loCfg = createobject("Empty")
        addproperty(loCfg, "empresa", "SbSoftware")
        this.SetResource("config", loCfg)
    endproc

    procedure OnApplicationStopping
        * Cerrar lo que se abrio.
    endproc

enddefine
"lifecycle": { "enabled": true, "classname": "MiErpLifecycleClass" }
olepublic ist nicht optional. Ohne es landet die Klasse nicht in der Typbibliothek der MTDLL und die Engine kann sie nicht aktivieren: im Log erscheint "Cannot create ApplicationLifecycle instance". Den classname im Manifest trägt der Hook beim Kompilieren ein, mit seinem Suffix - von Hand schreibt man ihn nicht.

Debuggen ohne Kompilieren - FoxServerBench

Das Werkzeug, das am meisten Zeit spart: es führt Ihren Controller ohne MTDLL, ohne Server und ohne http.sys aus, mit dem Debugger über Ihrer eigenen Quelle. Sie setzen SET STEP ON in Ihre Prozedur, starten die Bank und halten an, wo Sie wollen.

FoxServerBench: einen Endpunkt ohne Server ausführen
loBench = createobject("FoxServerBench")
loBench.SetProject("c:\misproyectos\ventas")
loBench.OnDeclare = "MiCarga"              && tu rutina: SET PROCEDURE, abrir tablas...
loBench.SetResource("config", loFalsa)     && simula lo que deja el lifecycle

loBench.Request("GET", "articulos/42")
loRes = loBench.Run()

? loRes.StatusCode
? loRes.Body
Drei Dinge, bevor Sie der Bank Lugen vorwerfen. JSONFox.fxp muss KOMPILIERT im build\ Ihres Projekts liegen: ohne sie kommt ein POST mit JSON-Korper mit req.json null an, und niemand warnt Sie. Die API verkettet nicht: Request(...).Run() funktioniert nicht, jeder Aufruf steht für sich. Und es ist eine Bank, kein Emulator: sie bildet weder Middleware noch Authentifizierung, CORS oder Nebenlaufigkeit ab. Ein Grun hier beweist, dass Ihre Logik auslieferbar ist, nicht dass der Endpunkt richtig authentifiziert.

Weiter: API-Referenz →