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

Aus OBS Wiki
Zur Navigation springen Zur Suche springen
(Die Seite wurde neu angelegt: „semesta4d [http://ww1.semesta88.net/ http://ww1.semesta88.net/]. Auckland Vapors histrion Rieko Ioane wads a strain during the First-rate Rugby collide against…“)
 
(Die Seite wurde neu angelegt: „{{Kostenpflichtige Module}} =Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz= Dieses Beispiel zeigt einen schreibenden Endpunkt fÜr eine mobile App: AuftrÄge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen Über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen Über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.…“)
 
Zeile 1: Zeile 1:
semesta4d [http://ww1.semesta88.net/ http://ww1.semesta88.net/]. Auckland Vapors histrion Rieko Ioane wads a strain during the First-rate Rugby collide against Dixieland Africa's Fundamental Cheetahs at Eden Parkland on Crataegus oxycantha 12, 2017<br><br>All Black candidate Rieko Ioane scored a look-alike as the Auckland Vapors left field the Central Cheetahs in their debris at Nirvana Commons on Fri.<br><br>The Blue devils jaggy ogdoad tries to foursome in the 50-32 win, earning themselves a lively incentive level as they try to remain alert in the ultra-private-enterprise Newfangled Seeland conference.<br><br>The Cheetahs were militant ahead of time on and an good impulsive sledgehammer earned them III tries, deuce of them to Hooker Elandre Huggett, big approximately reputability to the scoreboard.<br><br>But inadequate defense mechanism net ball the Bloemfontein-based team depressed as they slouched to their eighth square release.<br><br>Captain Francois Abdomen aforementioned the Cheetahs' Apteryx road misstep would not make whatever easier when they face up the in-organise Duke of Wellington Hurricanes succeeding workweek.<br><br>"We'll keep fighting and hopefully we can make a shock," he said.<br><br>It was the third come through in a course for the Blues, World Health Organization rest on the merchant ship of their group discussion one and only peak can the Otago Highlanders, who take in a lame in give on them.<br><br>With virtuoso center Sonny Pecker Williams retired injured, Rieko Ioane [https://Openclipart.org/search/?query=stepped stepped] into the limelight, enhancing his chances of a call-up for the British and Irish Lions Essay serial.<br><br>Established All Blacks Steven Luatua and Patrick Tuipulotu, returning from a [http://Www.answers.com/topic/binding binding] injury, also scored tries and performed good to hike their showcase for an outside call back.<br><br>The lead changed trine times in the too soon stages as the sides traded blows in a free-sleek game, with Stomach porta the grading when he constrained his means concluded the channel. Rieko Ioane reach game immediately, bursting cut down the offstage and finding himself with gain trial later on picking up Melani Nanai's hap.<br><br>Torsten vanguard Jaarsveld regained the extend for the Cheetahs from a driving sledge only shut up George C. Scott Scrafton snatched it plunk for when he barged yesteryear a flailing Raymond Rhule.<br><br>After a near 20 minutes, the Vapours henpecked the future 40, scoring half-dozen unrequited tries in front Huggett taloned peerless cover for the Cheetahs through with some other drive sledgehammer. The Vapors punished the Due south African as they pushed on attack, thieving the lump good their have product line and functional it the length of the playing field for Nanai to grade.<br><br>Huggett's endorse adjudicate in the final stage few proceedings was testament to the Cheetah's never-say-kick the bucket posture simply the resultant was already on the far side doubtfulness.
{{Kostenpflichtige Module}}
 
=Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz=
 
Dieses Beispiel zeigt einen schreibenden Endpunkt fÜr eine mobile App: AuftrÄge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen Über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen Über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.
 
==Einrichtung in OBS==
 
* '''Server-Profil:''' Standard-TLS-Profil ''Public-API'' (Port 443)
* '''Zugang:''' ''Mobile-App''
** API-Key: zufÄllig generiert
** JWT aktiv, JWT-Endpunkt ''auth'', JWT-Key zufÄllig, JWT-Exp 60 (Minuten)
* '''Endpunkte''' (beide dem Profil ''Public-API'' zugeordnet):
** ''orders/{uid}'' - Auftrag lesen/Ändern
** ''orders/{uid}/material'' - Material erfassen
* '''Berechtigung:''' Zugang ''Mobile-App'' fÜr beide Endpunkte freigeschaltet
 
==JWT-Authentifizierungs-Skript (Zugang)==
 
Stellt Mandant und Rollen als Custom-Claims aus und legt ein Refresh-Token an (das jti wird in einer eigenen Sperrtabelle abgelegt). Bei einem Refresh ruft der Server im selben Skript die Methode ''Refresh'' auf. Die Hilfsfunktionen (''PasswortPasst'', ''RefreshJti...'') sind illustrativ und projektabhÄngig.
 
<syntaxhighlight lang="pascal" line>
function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    oVal : TJSONValue;
    cUser, cPass, cRefreshJti: string;
begin
    oRes := TJSONObject.Create();
    try
        cUser := ''; cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username'); if (Assigned(oVal)) then cUser := oVal.Value;
            oVal := oBody.GetValue('password'); if (Assigned(oVal)) then cPass := oVal.Value;
        end;
 
        if (PasswortPasst(cUser, cPass)) then begin
            cRefreshJti := GlobalUID();
            RefreshJtiAblegen(cUser, cRefreshJti);  // eigene Sperrtabelle
 
            oRes.AddPair('status'              , 1);
            oRes.AddPair('_OBS_JWT_ID'          , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT'    , cUser);
            oRes.AddPair('_OBS_JWT_CLAIM_tenant', TechnikerMandant(cUser));
            oRes.AddPair('_OBS_JWT_CLAIM_roles' , TechnikerRollen(cUser));  // z.B. "tech,lead"
            oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cRefreshJti);
            // _OBS_JWT_REFRESH_EXP weggelassen -> Default 90 Tage
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
 
//------------------------------------------------------------------------------
 
function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
    cAltesJti, cNeuesJti, cUser: string;
begin
    oRes := TJSONObject.Create();
    try
        cAltesJti := oParams.Values['_OBS_JWT_ID'];      // altes Refresh-jti
        cUser    := oParams.Values['_OBS_JWT_SUBJECT'];
 
        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'          , cUser);
        oRes.AddPair('_OBS_JWT_SUBJECT'    , cUser);
        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>
 
==Endpunkt ''orders/{uid}'' - ändern mit ETag / If-Match==
 
''GET'' liefert den Auftrag samt ''ETag'' (Version), ''PUT'' prÜft Rolle und ''If-Match''.
 
<syntaxhighlight lang="pascal" line>
function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    cUid: string;
    nVer: integer;
begin
    oRes := TJSONObject.Create();
    try
        cUid := oParams.Values['_OBS_PATH_uid'];
        if (not AuftragLesen(oParams.Values['_OBS_JWT_CLAIM_tenant'], cUid, oRes, nVer)) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 404);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'NOT_FOUND'));
            result := oRes.ToJSON();
            exit;
        end;
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nVer));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
 
//------------------------------------------------------------------------------
 
function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    cUid: string;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        // nur Rolle "lead" darf Ändern - Rolle kommt aus dem Token, nicht aus dem Body
        if (Pos('lead', oParams.Values['_OBS_JWT_CLAIM_roles']) = 0) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 403);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'FORBIDDEN_ROLE'));
            result := oRes.ToJSON();
            exit;
        end;
 
        cUid    := oParams.Values['_OBS_PATH_uid'];
        nAktuell := AuftragVersion(cUid);
        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;
 
        AuftragSpeichern(cUid, oBody);  // setzt Version auf nAktuell + 1
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
==Endpunkt ''orders/{uid}/material'' - idempotentes Anlegen==
 
''POST'' erfasst eine Materialposition: Validierungsfehler -> 422, erfolgreiches Anlegen -> 201. Ein wiederholter Aufruf mit gleichem ''Idempotency-Key'' liefert dieselbe Antwort ohne Zweitbuchung. Der Idempotenz-Eintrag wird in derselben Transaktion wie die Buchung gespeichert.
 
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
    oVal: TJSONValue;
    cKey, cUid, cArtikel, cGespeichert, cNeueUid: string;
    nMenge: integer;
begin
    oRes := TJSONObject.Create();
    try
        cUid := oParams.Values['_OBS_PATH_uid'];
        cKey := oParams.Values['idempotency-key'];
 
        // 1) Idempotenz: gleicher Key bereits verarbeitet?
        if ((not Empty(cKey)) and IdempotenzAntwort(cKey, cGespeichert)) then begin
            result := cGespeichert;  // identische Antwort, keine Zweitbuchung
            exit;
        end;
 
        // 2) Validierung -> 422 mit traceId
        cArtikel := '';
        nMenge  := 0;
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('artikel'); if (Assigned(oVal)) then cArtikel := oVal.Value;
            oVal := oBody.GetValue('menge');  if (Assigned(oVal)) then nMenge  := iVal(oVal.Value);
        end;
        if ((Empty(cArtikel)) or (nMenge <= 0)) then begin
            oErr := TJSONObject.Create();
            oErr.AddPair('code'  , 'VALIDATION_FAILED');
            oErr.AddPair('message', 'artikel und menge sind Pflicht');
            oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
            oRes.AddPair('_OBS_HTTP_STATUS', 422);
            oRes.AddPair('error', oErr);
            result := oRes.ToJSON();
            exit;
        end;
 
        // 3) In EINER Transaktion: Position anlegen + Idempotenz-Eintrag committen
        //    (Transaktionssteuerung Über die OBS-Skript-DB-API)
        cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);
 
        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
 
        if (not Empty(cKey)) then begin
            IdempotenzSpeichern(cKey, result);  // in derselben Transaktion
        end;
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
==Test mit curl==
 
Token holen (Antwort enthÄlt token, refreshToken und serverTime):
 
curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
      -d "{\"username\":\"tech1\",\"password\":\"geheim\"}" ^
      https://api.meinserver.de/auth/
 
{"token":"eyJ...","refreshToken":"eyJ...","serverTime":"2026-06-29T15:30:12+02:00"}
 
Auftrag lesen (liefert den ETag-Header):
 
curl -i -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
      https://api.meinserver.de/orders/4711
... ETag: 7
 
ändern mit korrektem If-Match -> 200, mit veraltetem If-Match -> 409:
 
curl -i -X PUT -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
      -H "If-Match: 7" -H "Content-Type: application/json" ^
      -d "{\"status\":\"erledigt\"}" ^
      https://api.meinserver.de/orders/4711
 
Material idempotent erfassen (zweiter Aufruf mit gleichem Key -> gleiche Antwort, keine Doppelbuchung):
 
curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
      -H "Idempotency-Key: 9c84-7f2a-..." -H "Content-Type: application/json" ^
      -d "{\"artikel\":\"A100\",\"menge\":3}" ^
      https://api.meinserver.de/orders/4711/material
 
Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):
 
curl -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer <refreshToken>" ^
      https://api.meinserver.de/auth/
 
==Was zeigt das Beispiel?==
 
* Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422.
* Optimistic Concurrency Über ''ETag'' (GET) und ''If-Match'' (PUT) -> 409 VERSION_CONFLICT.
* Idempotente Schreibzugriffe Über den ''Idempotency-Key'' (Schutz gegen Doppelbuchung).
* RollenprÜfung aus dem Token-Claim ''_OBS_JWT_CLAIM_roles'', nicht aus dem Body.
* JWT mit Custom-Claims (''tenant''/''roles'') und Refresh-Token mit Rotation.
* ''traceId'' im Fehler-Body fÜr die Support-Nachverfolgung (auch als Header ''X-Trace-Id'').

Aktuelle Version vom 29. Juni 2026, 11:07 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


Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz

Dieses Beispiel zeigt einen schreibenden Endpunkt fÜr eine mobile App: AuftrÄge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen Über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen Über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.

Einrichtung in OBS

  • Server-Profil: Standard-TLS-Profil Public-API (Port 443)
  • Zugang: Mobile-App
    • API-Key: zufÄllig generiert
    • JWT aktiv, JWT-Endpunkt auth, JWT-Key zufÄllig, JWT-Exp 60 (Minuten)
  • Endpunkte (beide dem Profil Public-API zugeordnet):
    • orders/{uid} - Auftrag lesen/Ändern
    • orders/{uid}/material - Material erfassen
  • Berechtigung: Zugang Mobile-App fÜr beide Endpunkte freigeschaltet

JWT-Authentifizierungs-Skript (Zugang)

Stellt Mandant und Rollen als Custom-Claims aus und legt ein Refresh-Token an (das jti wird in einer eigenen Sperrtabelle abgelegt). Bei einem Refresh ruft der Server im selben Skript die Methode Refresh auf. Die Hilfsfunktionen (PasswortPasst, RefreshJti...) sind illustrativ und projektabhÄngig.

function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    oVal : TJSONValue;
    cUser, cPass, cRefreshJti: string;
begin
    oRes := TJSONObject.Create();
    try
        cUser := ''; cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username'); if (Assigned(oVal)) then cUser := oVal.Value;
            oVal := oBody.GetValue('password'); if (Assigned(oVal)) then cPass := oVal.Value;
        end;

        if (PasswortPasst(cUser, cPass)) then begin
            cRefreshJti := GlobalUID();
            RefreshJtiAblegen(cUser, cRefreshJti);   // eigene Sperrtabelle

            oRes.AddPair('status'               , 1);
            oRes.AddPair('_OBS_JWT_ID'          , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT'     , cUser);
            oRes.AddPair('_OBS_JWT_CLAIM_tenant', TechnikerMandant(cUser));
            oRes.AddPair('_OBS_JWT_CLAIM_roles' , TechnikerRollen(cUser));   // z.B. "tech,lead"
            oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cRefreshJti);
            // _OBS_JWT_REFRESH_EXP weggelassen -> Default 90 Tage
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

//------------------------------------------------------------------------------

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

        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'          , cUser);
        oRes.AddPair('_OBS_JWT_SUBJECT'     , cUser);
        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;

Endpunkt orders/{uid} - ändern mit ETag / If-Match

GET liefert den Auftrag samt ETag (Version), PUT prÜft Rolle und If-Match.

function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    cUid: string;
    nVer: integer;
begin
    oRes := TJSONObject.Create();
    try
        cUid := oParams.Values['_OBS_PATH_uid'];
        if (not AuftragLesen(oParams.Values['_OBS_JWT_CLAIM_tenant'], cUid, oRes, nVer)) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 404);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'NOT_FOUND'));
            result := oRes.ToJSON();
            exit;
        end;
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nVer));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

//------------------------------------------------------------------------------

function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    cUid: string;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        // nur Rolle "lead" darf Ändern - Rolle kommt aus dem Token, nicht aus dem Body
        if (Pos('lead', oParams.Values['_OBS_JWT_CLAIM_roles']) = 0) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 403);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'FORBIDDEN_ROLE'));
            result := oRes.ToJSON();
            exit;
        end;

        cUid     := oParams.Values['_OBS_PATH_uid'];
        nAktuell := AuftragVersion(cUid);
        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;

        AuftragSpeichern(cUid, oBody);   // setzt Version auf nAktuell + 1
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Endpunkt orders/{uid}/material - idempotentes Anlegen

POST erfasst eine Materialposition: Validierungsfehler -> 422, erfolgreiches Anlegen -> 201. Ein wiederholter Aufruf mit gleichem Idempotency-Key liefert dieselbe Antwort ohne Zweitbuchung. Der Idempotenz-Eintrag wird in derselben Transaktion wie die Buchung gespeichert.

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
    oVal: TJSONValue;
    cKey, cUid, cArtikel, cGespeichert, cNeueUid: string;
    nMenge: integer;
begin
    oRes := TJSONObject.Create();
    try
        cUid := oParams.Values['_OBS_PATH_uid'];
        cKey := oParams.Values['idempotency-key'];

        // 1) Idempotenz: gleicher Key bereits verarbeitet?
        if ((not Empty(cKey)) and IdempotenzAntwort(cKey, cGespeichert)) then begin
            result := cGespeichert;   // identische Antwort, keine Zweitbuchung
            exit;
        end;

        // 2) Validierung -> 422 mit traceId
        cArtikel := '';
        nMenge   := 0;
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('artikel'); if (Assigned(oVal)) then cArtikel := oVal.Value;
            oVal := oBody.GetValue('menge');   if (Assigned(oVal)) then nMenge   := iVal(oVal.Value);
        end;
        if ((Empty(cArtikel)) or (nMenge <= 0)) then begin
            oErr := TJSONObject.Create();
            oErr.AddPair('code'   , 'VALIDATION_FAILED');
            oErr.AddPair('message', 'artikel und menge sind Pflicht');
            oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
            oRes.AddPair('_OBS_HTTP_STATUS', 422);
            oRes.AddPair('error', oErr);
            result := oRes.ToJSON();
            exit;
        end;

        // 3) In EINER Transaktion: Position anlegen + Idempotenz-Eintrag committen
        //    (Transaktionssteuerung Über die OBS-Skript-DB-API)
        cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);

        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();

        if (not Empty(cKey)) then begin
            IdempotenzSpeichern(cKey, result);   // in derselben Transaktion
        end;
    finally
        MyFreeAndNil(oRes);
    end;
end;

Test mit curl

Token holen (Antwort enthÄlt token, refreshToken und serverTime):

curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
     -d "{\"username\":\"tech1\",\"password\":\"geheim\"}" ^
     https://api.meinserver.de/auth/
{"token":"eyJ...","refreshToken":"eyJ...","serverTime":"2026-06-29T15:30:12+02:00"}

Auftrag lesen (liefert den ETag-Header):

curl -i -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
     https://api.meinserver.de/orders/4711
... ETag: 7

ändern mit korrektem If-Match -> 200, mit veraltetem If-Match -> 409:

curl -i -X PUT -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
     -H "If-Match: 7" -H "Content-Type: application/json" ^
     -d "{\"status\":\"erledigt\"}" ^
     https://api.meinserver.de/orders/4711

Material idempotent erfassen (zweiter Aufruf mit gleichem Key -> gleiche Antwort, keine Doppelbuchung):

curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
     -H "Idempotency-Key: 9c84-7f2a-..." -H "Content-Type: application/json" ^
     -d "{\"artikel\":\"A100\",\"menge\":3}" ^
     https://api.meinserver.de/orders/4711/material

Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):

curl -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer <refreshToken>" ^
     https://api.meinserver.de/auth/

Was zeigt das Beispiel?

  • Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422.
  • Optimistic Concurrency Über ETag (GET) und If-Match (PUT) -> 409 VERSION_CONFLICT.
  • Idempotente Schreibzugriffe Über den Idempotency-Key (Schutz gegen Doppelbuchung).
  • RollenprÜfung aus dem Token-Claim _OBS_JWT_CLAIM_roles, nicht aus dem Body.
  • JWT mit Custom-Claims (tenant/roles) und Refresh-Token mit Rotation.
  • traceId im Fehler-Body fÜr die Support-Nachverfolgung (auch als Header X-Trace-Id).