OBS/Kostenpflichtige Module/RESTServer/Scripting: Unterschied zwischen den Versionen

Aus OBS Wiki
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
 
(2 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 75: Zeile 75:
|-
|-
| _OBS_JWT_AUDIENCE    || Audience (''aud''-Claim) - typisch Mandant / Rolle
| _OBS_JWT_AUDIENCE    || Audience (''aud''-Claim) - typisch Mandant / Rolle
|-
| _OBS_JWT_CLAIM_<name> || Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar
|-
| _OBS_TRACE_ID        || Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)
|}
|}


Zeile 92: Zeile 96:


Da der Präfix ''_OBS_'' für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].
Da der Präfix ''_OBS_'' für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].
===Datei-Uploads===
Ist der Endpunkt für Uploads freigeschaltet (<code>re_upload = 1</code>, siehe
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und
übergibt dem Skript Pfad, Originalname und Content-Type über reservierte
Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B.
DMS-Ablage). Der Body wird in diesem Fall '''nicht''' als JSON geparst - ''oBody''
ist ''nil''. Es gilt nicht das JSON-Body-Limit (10&nbsp;MB), sondern die pro
Endpunkt konfigurierte Grösse (''re_upload_size'', Standard 25&nbsp;MB;
Überschreitung -> 413).
Beide Übertragungsarten - <code>multipart/form-data</code> und der resumable
<code>Content-Range</code>-Upload - liefern dem Skript dieselben Parameter:
{| class="wikitable"
! Parameter !! Inhalt
|-
| _OBS_UPLOAD_PATH || Vollständiger Pfad zur temporären Datei auf dem Server
|-
| _OBS_UPLOAD_NAME || Originaldateiname
|-
| _OBS_UPLOAD_CONTENTTYPE || Content-Type der Datei (Default <code>application/octet-stream</code>, wenn der Client keinen angibt)
|}
Der '''Dateiname ist Pflicht'''. Fehlt er, lehnt der Server den Upload mit
'''400 Bad Request''' ab und das Skript wird nicht ausgeführt - das Skript kann
sich also darauf verlassen, dass ''_OBS_UPLOAD_PATH'' und ''_OBS_UPLOAD_NAME''
gesetzt sind, sobald es läuft.
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}}
====Einfacher Upload (multipart/form-data)====
Eine Datei pro Request. Name und Content-Type stammen aus der
<code>Content-Disposition</code> der Datei-Partie (<code>filename=</code>). Der
Server speichert die erste Datei-Partie und ruft das Skript auf:
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cPfad: string;
    cName: string;
    cTyp : string;
begin
    oRes := TJSONObject.Create();
    try
        cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
        cName := oParams.Values['_OBS_UPLOAD_NAME'];
        cTyp  := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];
        // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...
        oRes.AddPair('status'  , 'ok');
        oRes.AddPair('dateiname', cName);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
====Resumable/Chunked Upload (Content-Range)====
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem
Header <code>Content-Range: bytes START-END/TOTAL</code>. Der Server hängt die
Chunks '''sequentiell''' an eine Session-Datei an und behandelt unvollständige
Uploads selbst - das Endpunkt-Skript läuft '''erst beim vollständigen Upload'''
(dann mit gesetztem ''_OBS_UPLOAD_PATH'', ''_OBS_UPLOAD_NAME'' und
''_OBS_UPLOAD_CONTENTTYPE'').
Name und Content-Type sind im <code>Content-Range</code>-Protokoll nicht enthalten;
der Client liefert sie deshalb '''im ersten Chunk''' über den Header
<code>Upload-Metadata</code> (Format wie bei tus: kommagetrennte Paare
<code>schlüssel base64wert</code>, Werte Base64-kodiert):
<pre>
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm
</pre>
Erkannt werden die Schlüssel <code>filename</code> (Pflicht) und
<code>filetype</code> (optional). Fehlt <code>filename</code> im ersten Chunk,
wird der Upload mit '''400''' abgelehnt, ohne dass eine Datei angelegt wird.
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.
Ablauf (vom Client gesteuert, der Server antwortet jeweils):
{| class="wikitable"
! Request !! Server-Antwort
|-
| Erster Chunk <code>Content-Range: bytes 0-1048575/5000000</code> + <code>Upload-Metadata</code> || '''308''' Resume Incomplete + Header <code>Upload-Id</code>, <code>Upload-Offset</code>, <code>Range</code>
|-
| weitere Chunks (jeweils <code>Upload-Id</code> mitsenden) || '''308''' mit aktualisiertem <code>Upload-Offset</code>
|-
| letzter Chunk (Bereich erreicht TOTAL) || normale Skript-Antwort (z.B. '''200'''/'''201''')
|-
| Statusabfrage <code>Content-Range: bytes */5000000</code> || aktueller <code>Upload-Offset</code>, ohne anzuhängen (Resume)
|}
{| class="wikitable"
! Header !! Richtung !! Bedeutung
|-
| Content-Range || Request || <code>bytes START-END/TOTAL</code>; <code>bytes */TOTAL</code> nur als Statusabfrage
|-
| Upload-Metadata || Request || tus-Metadaten im ersten Chunk: <code>filename</code> (Pflicht), <code>filetype</code> (optional), Base64-kodiert
|-
| Upload-Id || Request/Response || Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden.
|-
| Upload-Offset || Response || Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)
|-
| Range || Response || Bereits gespeicherter Bereich (<code>bytes=0-N</code>)
|}
Der Server hängt einen Chunk '''nur an, wenn START dem bereits gespeicherten Stand
entspricht'''. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt,
sondern der aktuelle ''Upload-Offset'' zurückgemeldet - der Client setzt ab dort
neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (''re_upload_size''),
antwortet der Server mit '''413 Payload Too Large'''.
{{Hinweis|Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.}}


==Rückgabe==
==Rückgabe==
Zeile 107: Zeile 234:
         oRes.AddPair('wert_string', '123');
         oRes.AddPair('wert_string', '123');
         oRes.AddPair('wert_int'  , 456);
         oRes.AddPair('wert_int'  , 456);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
==Antwort steuern: Statuscode, Header, traceId==
Ein Skript kann den HTTP-Statuscode und beliebige Response-Header Über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.
{| class="wikitable"
! Antwort-Feld !! Wirkung
|-
| _OBS_HTTP_STATUS (Zahl) || HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet.
|-
| _OBS_HEADERS (Objekt) || Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection).
|}
Zusätzlich trÄgt jede Antwort den Header '''X-Trace-Id''' (Korrelations-ID). Dieselbe ID liegt dem Skript als ''oParams.Values['_OBS_TRACE_ID']'' vor und erscheint in jeder Server-Fehler-Logzeile in '''RESTSRV_PROTO''' - so lÄsst sich ein Fehler ohne GerÄtezugriff im Log wiederfinden.
===Beispiel: Anlegen mit 201, Location und ETag===
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
        oHdr.AddPair('ETag', '1');
        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('_OBS_HEADERS', oHdr);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
===Beispiel: Strukturierter Fehler mit Statuscode und traceId===
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oErr := TJSONObject.Create();
        oErr.AddPair('code', 'VALIDATION_FAILED');
        oErr.AddPair('message', 'Feld "menge" fehlt');
        oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
        oRes.AddPair('_OBS_HTTP_STATUS', 422);
        oRes.AddPair('error', oErr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
===Beispiel: Optimistic Concurrency (ETag / If-Match)===
Mit Statuscode, Headern und dem lesbaren Header ''If-Match'' fÜhrt das Skript je Datensatz einen VersionszÄhler und lehnt veraltete Schreibzugriffe mit 409 ab:
<syntaxhighlight lang="pascal" line>
function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
        nIfMatch := iVal(oParams.Values['if-match']);
        if (nIfMatch <> nAktuell) then begin
            oHdr := TJSONObject.Create();
            oHdr.AddPair('ETag', xStr(nAktuell));
            oRes.AddPair('_OBS_HTTP_STATUS', 409);
            oRes.AddPair('_OBS_HEADERS', oHdr);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
            result := oRes.ToJSON();
            exit;
        end;
        // ... speichern, Version hochzÄhlen ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
         result := oRes.ToJSON();
         result := oRes.ToJSON();
     finally
     finally
Zeile 167: Zeile 388:
| 9      || Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von ''error''
| 9      || Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von ''error''
|}
|}
===Custom-Claims, serverTime und Refresh===
Das Authenticate-Skript kann dem Token zusÄtzlich '''beliebige Custom-Claims''' mitgeben - z.B. Mandant und Rollen - und optional ein '''Refresh-Token''' anstoßen:
{| class="wikitable"
! RÜckgabefeld !! Bedeutung
|-
| _OBS_JWT_CLAIM_&lt;name&gt; || Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_&lt;name&gt;'].
|-
| _OBS_JWT_REFRESH_ID || (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.
|-
| _OBS_JWT_REFRESH_EXP || (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).
|}
Jedes Token trÄgt automatisch den Claim ''token_use'' (''access'' bzw. ''refresh''). Die Antwort enthÄlt bei Erfolg ''serverTime'' (ISO-8601 mit Offset), bei ausgestelltem Refresh zusÄtzlich ''refreshToken'':
{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
{{Hinweis|Mandant und Rollen immer aus dem Token lesen (''_OBS_JWT_CLAIM_*''), nie aus Body oder Query.}}
====Refresh-Methode====
Der Refresh lÄuft Über '''denselben JWT-Endpunkt''' und '''dasselbe Skript'''. Liegt am JWT-Endpunkt ein ''Authorization: Bearer <token>'' vor, ruft der Server statt ''Authenticate'' die Methode '''Refresh''' auf (sonst Login). Der Server verifiziert den Refresh-Token zuvor (Signatur, Ablauf, ''token_use=refresh''); das alte Refresh-jti steht als ''oParams.Values['_OBS_JWT_ID']'' bereit. Das Skript prÜft/rotiert seine Sperrtabelle und liefert wie ''Authenticate'' neue Claims zurÜck. Der Server stellt daraufhin ein rotiertes Token-Paar aus; ein bereits benutztes Refresh-Token wird mit 401 abgelehnt.
<syntaxhighlight lang="pascal" line>
function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
    cAltesJti, cNeuesJti: string;
begin
    oRes := TJSONObject.Create();
    try
        cAltesJti := oParams.Values['_OBS_JWT_ID'];
        // altes Refresh-jti gegen eigene Sperrtabelle prÜfen
        if (not RefreshJtiGueltig(cAltesJti)) then begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'AUTH_EXPIRED');
            result := oRes.ToJSON();
            exit;
        end;
        cNeuesJti := GlobalUID();
        RefreshJtiRotieren(cAltesJti, cNeuesJti);  // altes sperren, neues ablegen
        oRes.AddPair('status'              , 1);
        oRes.AddPair('_OBS_JWT_ID'          , GlobalUID());
        oRes.AddPair('_OBS_JWT_SUBJECT'    , oParams.Values['_OBS_JWT_SUBJECT']);
        oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
        oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
        oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cNeuesJti);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>


==Fehlerbehandlung im Skript==
==Fehlerbehandlung im Skript==
Zeile 212: Zeile 490:
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel: Datensatz anlegen mit JSON-Body]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel: Datensatz anlegen mit JSON-Body]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel: Pfad-Parameter im Routing]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel: Pfad-Parameter im Routing]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]

Aktuelle Version vom 30. Juni 2026, 12:28 Uhr

Kostenpflichtige Module

Internet-Shop
UPS
IMS Professional
SMS
Mehrlager-Verwaltung
Mehrsprachen Modul
Multilanguage Modul
EVA Marketing Tool
Termin-Projekte
Edifact-Schnittstelle
Backup Überwachung Email
OBS Geo Daten
DeliSprint / DPD
Filialen
Cashback
Moebelschnittstelle
Dokumenten Manager
DocuWare-Schnittstelle
OFML-Kalkulation
Versicherungsschaden
Gutschriftsanzeigen
Kameraverwaltung
DataInOut
OpenMasterData / IDS
Sammelpositionen


Anleitung für Endpunkt-Skripte

Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.

Sicherheitshinweise

HINWEIS: Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über DB_SQLVal bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.
  • Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).
  • Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.
  • Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in RESTSRV_PROTO.

Methoden-Signatur

Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt.

function Get   (oParams: TStrings; oBody: TJSONObject): string;
function Post  (oParams: TStrings; oBody: TJSONObject): string;
function Put   (oParams: TStrings; oBody: TJSONObject): string;
function Delete(oParams: TStrings; oBody: TJSONObject): string;
function Patch (oParams: TStrings; oBody: TJSONObject): string;

Nicht implementierte Methoden liefern automatisch 405-ähnliche Fehler über die generische Skript-Antwort.

Parameter

oParams (TStrings)

Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings.

Filterung durch den Server:

  • Geblockte Header werden nicht durchgereicht: authorization, cookie, proxy-authorization, x-forwarded-for, x-real-ip, apikey, api_key.
  • Parameter mit Präfix _OBS_ können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert.
  • Werte werden auf max. 1024 Zeichen begrenzt.
  • Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.

Zugriff im Skript:

if (not Empty(oParams.Values['kundennr'])) then begin
    cKundenNr := oParams.Values['kundennr'];
end;

oBody (TJSONObject)

Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird nil übergeben.

cUser := ;
cPass := ;
if (Assigned(oBody)) then begin
    oVal := oBody.GetValue('username');
    if (Assigned(oVal)) then begin
        cUser := oVal.Value;
    end;
    oVal := oBody.GetValue('password');
    if (Assigned(oVal)) then begin
        cPass := oVal.Value;
    end;
end;

Maximale Body-Grösse: 10 MB.

Reservierte _OBS_-Parameter

Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung:

Parameter Inhalt
_OBS_JWT_ID JWT-Id (jti-Claim) - typisch die User-Id
_OBS_JWT_SUBJECT Subject (sub-Claim) - typisch Benutzername
_OBS_JWT_AUDIENCE Audience (aud-Claim) - typisch Mandant / Rolle
_OBS_JWT_CLAIM_<name> Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar
_OBS_TRACE_ID Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)

Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.

Pfad-Parameter

Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe Endpunkte), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix _OBS_PATH_ in oParams bereit:

Template Zugriff im Skript
/orders/{uid} oParams.Values['_OBS_PATH_uid']
/orders/{uid}/modules/{code} oParams.Values['_OBS_PATH_uid'], oParams.Values['_OBS_PATH_code']

Da der Präfix _OBS_ für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt Beispiel 4 - Pfad-Parameter.

Datei-Uploads

Ist der Endpunkt für Uploads freigeschaltet (re_upload = 1, siehe Endpunkte), nimmt der Server hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und übergibt dem Skript Pfad, Originalname und Content-Type über reservierte Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B. DMS-Ablage). Der Body wird in diesem Fall nicht als JSON geparst - oBody ist nil. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro Endpunkt konfigurierte Grösse (re_upload_size, Standard 25 MB; Überschreitung -> 413).

Beide Übertragungsarten - multipart/form-data und der resumable Content-Range-Upload - liefern dem Skript dieselben Parameter:

Parameter Inhalt
_OBS_UPLOAD_PATH Vollständiger Pfad zur temporären Datei auf dem Server
_OBS_UPLOAD_NAME Originaldateiname
_OBS_UPLOAD_CONTENTTYPE Content-Type der Datei (Default application/octet-stream, wenn der Client keinen angibt)

Der Dateiname ist Pflicht. Fehlt er, lehnt der Server den Upload mit 400 Bad Request ab und das Skript wird nicht ausgeführt - das Skript kann sich also darauf verlassen, dass _OBS_UPLOAD_PATH und _OBS_UPLOAD_NAME gesetzt sind, sobald es läuft.

HINWEIS: Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.

Einfacher Upload (multipart/form-data)

Eine Datei pro Request. Name und Content-Type stammen aus der Content-Disposition der Datei-Partie (filename=). Der Server speichert die erste Datei-Partie und ruft das Skript auf:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cPfad: string;
    cName: string;
    cTyp : string;
begin
    oRes := TJSONObject.Create();
    try
        cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
        cName := oParams.Values['_OBS_UPLOAD_NAME'];
        cTyp  := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];

        // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...

        oRes.AddPair('status'   , 'ok');
        oRes.AddPair('dateiname', cName);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Resumable/Chunked Upload (Content-Range)

Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem Header Content-Range: bytes START-END/TOTAL. Der Server hängt die Chunks sequentiell an eine Session-Datei an und behandelt unvollständige Uploads selbst - das Endpunkt-Skript läuft erst beim vollständigen Upload (dann mit gesetztem _OBS_UPLOAD_PATH, _OBS_UPLOAD_NAME und _OBS_UPLOAD_CONTENTTYPE).

Name und Content-Type sind im Content-Range-Protokoll nicht enthalten; der Client liefert sie deshalb im ersten Chunk über den Header Upload-Metadata (Format wie bei tus: kommagetrennte Paare schlüssel base64wert, Werte Base64-kodiert):

Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm

Erkannt werden die Schlüssel filename (Pflicht) und filetype (optional). Fehlt filename im ersten Chunk, wird der Upload mit 400 abgelehnt, ohne dass eine Datei angelegt wird. Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.

Ablauf (vom Client gesteuert, der Server antwortet jeweils):

Request Server-Antwort
Erster Chunk Content-Range: bytes 0-1048575/5000000 + Upload-Metadata 308 Resume Incomplete + Header Upload-Id, Upload-Offset, Range
weitere Chunks (jeweils Upload-Id mitsenden) 308 mit aktualisiertem Upload-Offset
letzter Chunk (Bereich erreicht TOTAL) normale Skript-Antwort (z.B. 200/201)
Statusabfrage Content-Range: bytes */5000000 aktueller Upload-Offset, ohne anzuhängen (Resume)
Header Richtung Bedeutung
Content-Range Request bytes START-END/TOTAL; bytes */TOTAL nur als Statusabfrage
Upload-Metadata Request tus-Metadaten im ersten Chunk: filename (Pflicht), filetype (optional), Base64-kodiert
Upload-Id Request/Response Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden.
Upload-Offset Response Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)
Range Response Bereits gespeicherter Bereich (bytes=0-N)

Der Server hängt einen Chunk nur an, wenn START dem bereits gespeicherten Stand entspricht. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, sondern der aktuelle Upload-Offset zurückgemeldet - der Client setzt ab dort neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.

Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (re_upload_size), antwortet der Server mit 413 Payload Too Large.

HINWEIS: Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.

Rückgabe

Die Rückgabe ist immer ein JSON-String. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit {}. Der Server setzt Content-Type auf application/json; charset=utf-8 und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert.

Einfaches Beispiel:

function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oRes.AddPair('wert_string', '123');
        oRes.AddPair('wert_int'   , 456);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Antwort steuern: Statuscode, Header, traceId

Ein Skript kann den HTTP-Statuscode und beliebige Response-Header Über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.

Antwort-Feld Wirkung
_OBS_HTTP_STATUS (Zahl) HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet.
_OBS_HEADERS (Objekt) Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection).

Zusätzlich trÄgt jede Antwort den Header X-Trace-Id (Korrelations-ID). Dieselbe ID liegt dem Skript als oParams.Values['_OBS_TRACE_ID'] vor und erscheint in jeder Server-Fehler-Logzeile in RESTSRV_PROTO - so lÄsst sich ein Fehler ohne GerÄtezugriff im Log wiederfinden.

Beispiel: Anlegen mit 201, Location und ETag

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
        oHdr.AddPair('ETag', '1');

        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('_OBS_HEADERS', oHdr);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Beispiel: Strukturierter Fehler mit Statuscode und traceId

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oErr := TJSONObject.Create();
        oErr.AddPair('code', 'VALIDATION_FAILED');
        oErr.AddPair('message', 'Feld "menge" fehlt');
        oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);

        oRes.AddPair('_OBS_HTTP_STATUS', 422);
        oRes.AddPair('error', oErr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Beispiel: Optimistic Concurrency (ETag / If-Match)

Mit Statuscode, Headern und dem lesbaren Header If-Match fÜhrt das Skript je Datensatz einen VersionszÄhler und lehnt veraltete Schreibzugriffe mit 409 ab:

function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
        nIfMatch := iVal(oParams.Values['if-match']);

        if (nIfMatch <> nAktuell) then begin
            oHdr := TJSONObject.Create();
            oHdr.AddPair('ETag', xStr(nAktuell));
            oRes.AddPair('_OBS_HTTP_STATUS', 409);
            oRes.AddPair('_OBS_HEADERS', oHdr);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
            result := oRes.ToJSON();
            exit;
        end;

        // ... speichern, Version hochzÄhlen ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

JWT-Authentifizierungs-Skript

Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode Authenticate bereitstellen:

function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes  : TJSONObject;
    cUser : string;
    cPass : string;
begin
    oRes := TJSONObject.Create();
    try
        // Eingangsdaten lesen (Body oder Query-Param)
        cUser := '';
        cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username');
            if (Assigned(oVal)) then begin
                cUser := oVal.Value;
            end;
            oVal := oBody.GetValue('password');
            if (Assigned(oVal)) then begin
                cPass := oVal.Value;
            end;
        end;

        // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...
        if (PasswortPasst(cUser, cPass)) then begin
            oRes.AddPair('status'          , 1);
            oRes.AddPair('_OBS_JWT_ID'     , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT', cUser);
            oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1');
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;

        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Rückgabewerte:

status Bedeutung
1 Erfolgreich, der Server erzeugt aus den _OBS_JWT_*-Werten einen Token (HS256)
9 Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von error

Custom-Claims, serverTime und Refresh

Das Authenticate-Skript kann dem Token zusÄtzlich beliebige Custom-Claims mitgeben - z.B. Mandant und Rollen - und optional ein Refresh-Token anstoßen:

RÜckgabefeld Bedeutung
_OBS_JWT_CLAIM_<name> Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_<name>'].
_OBS_JWT_REFRESH_ID (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.
_OBS_JWT_REFRESH_EXP (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).

Jedes Token trÄgt automatisch den Claim token_use (access bzw. refresh). Die Antwort enthÄlt bei Erfolg serverTime (ISO-8601 mit Offset), bei ausgestelltem Refresh zusÄtzlich refreshToken:

{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
HINWEIS: Mandant und Rollen immer aus dem Token lesen (_OBS_JWT_CLAIM_*), nie aus Body oder Query.

Refresh-Methode

Der Refresh lÄuft Über denselben JWT-Endpunkt und dasselbe Skript. Liegt am JWT-Endpunkt ein Authorization: Bearer <token> vor, ruft der Server statt Authenticate die Methode Refresh auf (sonst Login). Der Server verifiziert den Refresh-Token zuvor (Signatur, Ablauf, token_use=refresh); das alte Refresh-jti steht als oParams.Values['_OBS_JWT_ID'] bereit. Das Skript prÜft/rotiert seine Sperrtabelle und liefert wie Authenticate neue Claims zurÜck. Der Server stellt daraufhin ein rotiertes Token-Paar aus; ein bereits benutztes Refresh-Token wird mit 401 abgelehnt.

function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
    cAltesJti, cNeuesJti: string;
begin
    oRes := TJSONObject.Create();
    try
        cAltesJti := oParams.Values['_OBS_JWT_ID'];

        // altes Refresh-jti gegen eigene Sperrtabelle prÜfen
        if (not RefreshJtiGueltig(cAltesJti)) then begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'AUTH_EXPIRED');
            result := oRes.ToJSON();
            exit;
        end;

        cNeuesJti := GlobalUID();
        RefreshJtiRotieren(cAltesJti, cNeuesJti);   // altes sperren, neues ablegen

        oRes.AddPair('status'               , 1);
        oRes.AddPair('_OBS_JWT_ID'          , GlobalUID());
        oRes.AddPair('_OBS_JWT_SUBJECT'     , oParams.Values['_OBS_JWT_SUBJECT']);
        oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
        oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
        oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cNeuesJti);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Fehlerbehandlung im Skript

Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 Interner Fehler und protokolliert die Detail-Meldung in RESTSRV_PROTO (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.

Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, ist eine eigene JSON-Struktur zu liefern:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        if (Empty(oParams.Values['kundennr'])) then begin
            oRes.AddPair('status', 'error');
            oRes.AddPair('msg'   , 'Parameter ''kundennr'' fehlt');
            result := oRes.ToJSON();
            exit;
        end;
        // ...
    finally
        MyFreeAndNil(oRes);
    end;
end;

Skript-Cache

Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: sys_date des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.

Verfügbare Bibliotheken

Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:

  • Base.Tools - String-, Datum-, IIF-, Empty-Helper
  • Base.DB / Base.xQuery - Datenbank-Operationen, DB_SQLVal, DB_SOpen, DB_LSeek
  • Base.qSqlReg - qSqlInit, qSet, SaveData
  • System.JSON - JSON-Objekte und -Arrays
  • Base.ToolsConst - Konstanten wie CRLF, SINGELQUOTE, EMPTY_DATE

Konkrete Beispiele: