<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="de">
	<id>https://wiki.bergau.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=MINERVA-Rademacker</id>
	<title>OBS Wiki - Benutzerbeiträge [de]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.bergau.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=MINERVA-Rademacker"/>
	<link rel="alternate" type="text/html" href="https://wiki.bergau.de/Spezial:Beitr%C3%A4ge/MINERVA-Rademacker"/>
	<updated>2026-09-26T06:59:56Z</updated>
	<subtitle>Benutzerbeiträge</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Datei-Upload&amp;diff=65547</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Datei-Upload</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Datei-Upload&amp;diff=65547"/>
		<updated>2026-09-25T09:28:34Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Reader-Aufrufe statt _OBS_UPLOAD_-Felder, Idempotency-Key-Pflicht bei Uploads ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
= Datei-Upload =&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt nimmt Dateien nur entgegen, wenn er dafür freigeschaltet ist&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;, siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]). Dann gilt nicht&lt;br /&gt;
das JSON-Body-Limit von 10&amp;amp;nbsp;MB, sondern die pro Endpunkt konfigurierte Grösse&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB; Überschreitung -&amp;gt; &#039;&#039;&#039;413&#039;&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
Diese Seite beschreibt die &#039;&#039;&#039;Übertragung&#039;&#039;&#039; - was ein Client senden muss und was&lt;br /&gt;
der Server antwortet. Was das &#039;&#039;&#039;Skript&#039;&#039;&#039; davon sieht, sind vier Aufrufe&lt;br /&gt;
(&amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadName()&amp;lt;/code&amp;gt;,&lt;br /&gt;
&amp;lt;code&amp;gt;UploadType()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadSha()&amp;lt;/code&amp;gt;) und steht unter&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting#Datei-Uploads|Scripting]].&lt;br /&gt;
Ein vollständiges Beispiel zeigt&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]].&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Server legt die Datei in einem temporären Verzeichnis ab und&lt;br /&gt;
&#039;&#039;&#039;verschiebt sie nicht selbst&#039;&#039;&#039;. Übernimmt das Skript sie nicht an ihren&lt;br /&gt;
Zielort, bleibt sie dort liegen.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein Upload läuft über &#039;&#039;POST&#039;&#039; und braucht deshalb - wie jeder&lt;br /&gt;
schreibende Aufruf - den Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt;. Fehlt er, antwortet&lt;br /&gt;
der Server mit &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;, und zwar bevor eine&lt;br /&gt;
Datei entgegengenommen wird. Beim resumable Upload gehört er an &#039;&#039;&#039;jedes&#039;&#039;&#039;&lt;br /&gt;
Teilstück; wirksam wird er beim letzten, weil erst dort eine Antwort entsteht, die&lt;br /&gt;
sich speichern lässt. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt&lt;br /&gt;
Idempotenz.}}&lt;br /&gt;
&lt;br /&gt;
== Einfacher Upload (multipart/form-data) ==&lt;br /&gt;
&lt;br /&gt;
Eine Datei pro Request. Name und Content-Type stammen aus der&lt;br /&gt;
&amp;lt;code&amp;gt;Content-Disposition&amp;lt;/code&amp;gt; der Datei-Partie (&amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt;). Der&lt;br /&gt;
Server speichert die erste Datei-Partie und ruft das Skript auf.&lt;br /&gt;
&lt;br /&gt;
Die &#039;&#039;&#039;übrigen Partien&#039;&#039;&#039; des Formulars - alles ohne &amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt; -&lt;br /&gt;
liest das Skript über &amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;. Der Wert ist UTF-8 und&lt;br /&gt;
auf 1024 Zeichen begrenzt, der Name auf Buchstaben, Ziffern und Unterstrich&lt;br /&gt;
reduziert. Zusatzangaben lassen sich damit im selben Request mitschicken, ohne sie&lt;br /&gt;
an die URL zu hängen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cPfad : string;&lt;br /&gt;
    cName : string;&lt;br /&gt;
    cTyp  : string;&lt;br /&gt;
    cBemerk: string;&lt;br /&gt;
begin&lt;br /&gt;
    cPfad   := oReader.UploadPath();&lt;br /&gt;
    cName   := oReader.UploadName();&lt;br /&gt;
    cTyp    := oReader.UploadType();&lt;br /&gt;
    cBemerk := oReader.Form(&#039;bemerkung&#039;);   // Formularfeld derselben Uebertragung&lt;br /&gt;
&lt;br /&gt;
    // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;   , &#039;ok&#039;);&lt;br /&gt;
    oWriter.Str(&#039;dateiname&#039;, cName);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Resumable/Chunked Upload (Content-Range) ==&lt;br /&gt;
&lt;br /&gt;
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem&lt;br /&gt;
Header &amp;lt;code&amp;gt;Content-Range: bytes START-END/TOTAL&amp;lt;/code&amp;gt;. Der Server hängt die&lt;br /&gt;
Chunks &#039;&#039;&#039;sequentiell&#039;&#039;&#039; an eine Session-Datei an und behandelt unvollständige&lt;br /&gt;
Uploads selbst - das Endpunkt-Skript läuft &#039;&#039;&#039;erst beim vollständigen Upload&#039;&#039;&#039;,&lt;br /&gt;
und dann liefern &#039;&#039;oReader.UploadPath()&#039;&#039;, &#039;&#039;UploadName()&#039;&#039; und &#039;&#039;UploadType()&#039;&#039;&lt;br /&gt;
dasselbe wie beim einfachen Upload. Für die Teilstücke läuft es &#039;&#039;&#039;nicht&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Name und Content-Type sind im &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;-Protokoll nicht enthalten;&lt;br /&gt;
der Client liefert sie deshalb &#039;&#039;&#039;im ersten Chunk&#039;&#039;&#039; über den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt; (Format wie bei tus: kommagetrennte Paare&lt;br /&gt;
&amp;lt;code&amp;gt;schlüssel base64wert&amp;lt;/code&amp;gt;, Werte Base64-kodiert):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Erkannt werden die Schlüssel &amp;lt;code&amp;gt;filename&amp;lt;/code&amp;gt; (Pflicht) und&lt;br /&gt;
&amp;lt;code&amp;gt;filetype&amp;lt;/code&amp;gt; (optional). Fehlt &amp;lt;code&amp;gt;filename&amp;lt;/code&amp;gt; im ersten Chunk,&lt;br /&gt;
wird der Upload mit &#039;&#039;&#039;400&#039;&#039;&#039; abgelehnt, ohne dass eine Datei angelegt wird.&lt;br /&gt;
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.&lt;br /&gt;
&lt;br /&gt;
Ablauf (vom Client gesteuert, der Server antwortet jeweils):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Request !! Server-Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Erster Chunk &amp;lt;code&amp;gt;Content-Range: bytes 0-1048575/5000000&amp;lt;/code&amp;gt; + &amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt; || &#039;&#039;&#039;308&#039;&#039;&#039; Resume Incomplete + Header &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| weitere Chunks (jeweils &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt; mitsenden) || &#039;&#039;&#039;308&#039;&#039;&#039; mit aktualisiertem &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| letzter Chunk (Bereich erreicht TOTAL) || normale Skript-Antwort (z.B. &#039;&#039;&#039;200&#039;&#039;&#039;/&#039;&#039;&#039;201&#039;&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| Statusabfrage &amp;lt;code&amp;gt;Content-Range: bytes */5000000&amp;lt;/code&amp;gt; || aktueller &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt;, ohne anzuhängen (Resume)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Header !! Richtung !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Content-Range || Request || &amp;lt;code&amp;gt;bytes START-END/TOTAL&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;bytes */TOTAL&amp;lt;/code&amp;gt; nur als Statusabfrage&lt;br /&gt;
|-&lt;br /&gt;
| Upload-Metadata || Request || tus-Metadaten im ersten Chunk: &amp;lt;code&amp;gt;filename&amp;lt;/code&amp;gt; (Pflicht), &amp;lt;code&amp;gt;filetype&amp;lt;/code&amp;gt; (optional), Base64-kodiert&lt;br /&gt;
|-&lt;br /&gt;
| 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.&lt;br /&gt;
|-&lt;br /&gt;
| Upload-Offset || Response || Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)&lt;br /&gt;
|-&lt;br /&gt;
| Range || Response || Bereits gespeicherter Bereich (&amp;lt;code&amp;gt;bytes=0-N&amp;lt;/code&amp;gt;)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Server hängt einen Chunk &#039;&#039;&#039;nur an, wenn START dem bereits gespeicherten Stand&lt;br /&gt;
entspricht&#039;&#039;&#039;. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt,&lt;br /&gt;
sondern der aktuelle &#039;&#039;Upload-Offset&#039;&#039; zurückgemeldet - der Client setzt ab dort&lt;br /&gt;
neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.&lt;br /&gt;
&lt;br /&gt;
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (&#039;&#039;re_upload_size&#039;&#039;),&lt;br /&gt;
antwortet der Server mit &#039;&#039;&#039;413 Payload Too Large&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.}}&lt;br /&gt;
&lt;br /&gt;
== Komprimierte Uploads ==&lt;br /&gt;
&lt;br /&gt;
Ein Upload-Körper mit dem Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;415 Unsupported Media Type&#039;&#039;&#039; abgewiesen - bei beiden Verfahren, einfach wie chunked.&lt;br /&gt;
&lt;br /&gt;
Der Grund ist nicht Bequemlichkeit. Bei einem chunked Upload bliebe offen, ob &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; die gepackten oder die rohen Bytes zählt, und die SHA256-Prüfsumme bezöge sich nach einem Entpacken auf eine andere Datei, als der Client gesendet hat. Etablierte Protokolle - tus, S3 Multipart, Google Resumable Upload, Azure Blob - behandeln Kompression aus demselben Grund als Eigenschaft der &#039;&#039;&#039;gespeicherten Datei&#039;&#039;&#039; und nicht der Übertragung.&lt;br /&gt;
&lt;br /&gt;
Wer gepackt hochladen will, packt die Datei &#039;&#039;&#039;selbst&#039;&#039;&#039; und lädt sie als &amp;lt;code&amp;gt;export.csv.gz&amp;lt;/code&amp;gt; hoch. Offsets, Prüfsumme und Wiederaufnahme stimmen dann, und das Endpunkt-Skript bekommt Name und Typ wie gewohnt über &amp;lt;code&amp;gt;oReader.UploadName()&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;oReader.UploadType()&amp;lt;/code&amp;gt;.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=65546</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zugaenge</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=65546"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Fehlercode AUTH_FAILED und Obergrenze der Refresh-Laufzeit ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Zugänge=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Zugang&#039;&#039;&#039; (Account) ist der Kanal, über den ein externer Konsument den REST-Server anspricht. Er bündelt API-Key, Host-Beschränkung, CORS-Konfiguration, optionale JWT-Authentifizierung und optionale mTLS-Subject-Prüfung.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Zugänge&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld          !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Name          || Sprechender Name, erscheint im Protokoll und der Statistik&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv         || Inaktive Zugänge werden bei der Authentifizierung mit 403 abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| API-Key       || Eindeutiger Schlüssel, den der Konsument im HTTP-Header &#039;&#039;apikey&#039;&#039; senden muss. Per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt&lt;br /&gt;
|-&lt;br /&gt;
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch &#039;&#039;;&#039;&#039; getrennt. &#039;&#039;&#039;Leer heisst nicht „keine Einschränkung&amp;quot;, sondern „kein Browser-Zugriff&amp;quot;&#039;&#039;&#039; - siehe unten&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)&lt;br /&gt;
|-&lt;br /&gt;
| JWT           || Schalter, ob für diesen Zugang ein JWT-Token nötig ist&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Endpunkt  || Pfad, über den &#039;&#039;&#039;alle Token-Vorgänge&#039;&#039;&#039; laufen: Anmelden, Erneuern und Abmelden (z.B. &#039;&#039;oauth&#039;&#039; oder &#039;&#039;meineapp/dev/auth&#039;&#039;). Verglichen wird der &#039;&#039;&#039;vollständige Pfad&#039;&#039;&#039;, mehrstufige Angaben mit &#039;&#039;/&#039;&#039; sind also erlaubt&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Key       || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Exp       || Gültigkeitsdauer eines ausgestellten Access-Tokens in &#039;&#039;&#039;Minuten&#039;&#039;&#039;. Derselbe Wert wird dem Konsumenten in der Token-Antwort als &#039;&#039;expiresIn&#039;&#039; in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; mitgeteilt und steht dem Endpunkt-Skript als &#039;&#039;_OBS_JWT_EXP_SEC&#039;&#039; zur Verfügung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==API-Key==&lt;br /&gt;
&lt;br /&gt;
Der API-Key ist die Basis-Authentifizierung. Der Client übergibt ihn im HTTP-Header:&lt;br /&gt;
&lt;br /&gt;
 apikey: abcd-efgh-ijkl-mnop-qrstuvwxyz12&lt;br /&gt;
&lt;br /&gt;
Wird ein Aufruf ohne &#039;&#039;apikey&#039;&#039;-Header gestellt, sucht der Server intern nach einem Zugang mit API-Key &#039;&#039;&#039;*&#039;&#039;&#039; - damit lassen sich (mit Vorsicht!) auch öffentliche Endpunkte ohne Authentifizierung realisieren.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Zugang mit API-Key &#039;&#039;*&#039;&#039; sollte nur für bewusst freigegebene Endpunkte verwendet werden und immer in Kombination mit einer Host-Beschränkung oder CORS-Beschränkung betrieben werden.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Nur für wirklich öffentliche Dateien.&#039;&#039;&#039; Ohne API-Key kennt der&lt;br /&gt;
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung&lt;br /&gt;
festmachen könnte ausser dem, was in der URL steht. Ein Wildcard-Zugang eignet&lt;br /&gt;
sich deshalb für Artefakte, die für jeden bestimmt sind (Test-APK, Handbuch),&lt;br /&gt;
&#039;&#039;&#039;nicht&#039;&#039;&#039; für Kundenbelege. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei einem Wildcard-Zugang sollte &#039;&#039;CORS-Origin&#039;&#039; &#039;&#039;&#039;leer&#039;&#039;&#039; bleiben.&lt;br /&gt;
Ein &#039;&#039;*&#039;&#039; dort wirkt auf die Preflight-Antwort &#039;&#039;&#039;aller&#039;&#039;&#039; Zugänge (siehe unten);&lt;br /&gt;
ein Download über die Adresszeile des Browsers sendet ohnehin kein &#039;&#039;Origin&#039;&#039;,&lt;br /&gt;
CORS greift dort also gar nicht.}}&lt;br /&gt;
&lt;br /&gt;
==Host-Beschränkung==&lt;br /&gt;
&lt;br /&gt;
Ist im Feld &#039;&#039;&#039;Host&#039;&#039;&#039; ein Wert hinterlegt, akzeptiert der Server Anfragen unter diesem Zugang nur dann, wenn die Remote-IP exakt diesem Host entspricht oder durch DNS-Auflösung auf diese IP zeigt. DNS-Auflösungen werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
==CORS==&lt;br /&gt;
&lt;br /&gt;
Soll ein Endpunkt aus einer Browser-Anwendung (Single-Page-App, JavaScript) heraus aufgerufen werden, muss das Origin der Anwendung in der CORS-Liste des Zugangs eingetragen sein.&lt;br /&gt;
&lt;br /&gt;
Format der Liste:&lt;br /&gt;
&lt;br /&gt;
 https://app.kunde.de;https://test.kunde.de;https://localhost:8080&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# Browser sendet einen Preflight-Request (&#039;&#039;OPTIONS&#039;&#039;). Der REST-Server prüft das Origin gegen die globale Origin-Liste &#039;&#039;&#039;aller&#039;&#039;&#039; Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.&lt;br /&gt;
# Der eigentliche Request wird gegen die Origin-Liste &#039;&#039;&#039;des aktuell verwendeten Zugangs&#039;&#039;&#039; geprüft.&lt;br /&gt;
# Stimmt das Origin nicht, wird mit 403 &#039;&#039;Origin nicht erlaubt für Account ...&#039;&#039; abgelehnt.&lt;br /&gt;
&lt;br /&gt;
Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen&lt;br /&gt;
&#039;&#039;apikey&#039;&#039; mit, der Zugang ist zu diesem Zeitpunkt unbekannt. &#039;&#039;&#039;Die Durchsetzung&lt;br /&gt;
liegt deshalb auf Schritt 2&#039;&#039;&#039;, nicht auf dem Preflight. Praktische Folge: trägt&lt;br /&gt;
&#039;&#039;&#039;ein&#039;&#039;&#039; Zugang auf dem Server ein &#039;&#039;*&#039;&#039; in seiner Liste, antwortet jeder&lt;br /&gt;
Preflight mit &#039;&#039;Access-Control-Allow-Origin: *&#039;&#039; - auch für Zugänge mit engerer&lt;br /&gt;
Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.&lt;br /&gt;
&lt;br /&gt;
Erlaubte Methoden: &#039;&#039;GET, POST, PUT, DELETE, PATCH&#039;&#039; (systemseitig festgelegt).&lt;br /&gt;
&lt;br /&gt;
Bei den &#039;&#039;&#039;Anfrage-Headern&#039;&#039;&#039; spiegelt der Server im Preflight zurück, was der&lt;br /&gt;
Browser in &#039;&#039;Access-Control-Request-Headers&#039;&#039; angefragt hat. Damit sind eigene&lt;br /&gt;
Header eines Projekts - &#039;&#039;Idempotency-Key&#039;&#039;, &#039;&#039;If-Match&#039;&#039;, später&lt;br /&gt;
&#039;&#039;Upload-Offset&#039;&#039;/&#039;&#039;Upload-Length&#039;&#039; - ohne Änderung am Server nutzbar. Fragt der&lt;br /&gt;
Preflight keine Header an, antwortet der Server mit der Liste&lt;br /&gt;
&#039;&#039;Content-Type, apikey, Authorization, Accept&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Header-Liste ist &#039;&#039;&#039;keine Schutzgrenze&#039;&#039;&#039; - das ist das Origin. Sie&lt;br /&gt;
sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die&lt;br /&gt;
Zugangsdaten dieser Schnittstelle in Headern stehen (&#039;&#039;apikey&#039;&#039;,&lt;br /&gt;
&#039;&#039;Authorization&#039;&#039;) und nicht in Cookies: es gibt also keine automatisch&lt;br /&gt;
mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.}}&lt;br /&gt;
&lt;br /&gt;
===Was „leer&amp;quot; bedeutet===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein &#039;&#039;&#039;leeres&#039;&#039;&#039; Feld CORS-Origins heisst &#039;&#039;&#039;nicht&#039;&#039;&#039; „keine&lt;br /&gt;
Einschränkung&amp;quot;. Es heisst: &#039;&#039;&#039;jeder Aufruf mit &#039;&#039;Origin&#039;&#039;-Header wird mit 403&lt;br /&gt;
abgelehnt&#039;&#039;&#039;. Für Clients &#039;&#039;&#039;ohne&#039;&#039;&#039; &#039;&#039;Origin&#039;&#039;-Header - mobile Apps, Server,&lt;br /&gt;
curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.}}&lt;br /&gt;
&lt;br /&gt;
Daraus ergeben sich drei sinnvolle Einstellungen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| leer || Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden&lt;br /&gt;
|-&lt;br /&gt;
| konkrete Origins || Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;*&#039;&#039; || Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key&lt;br /&gt;
zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste&lt;br /&gt;
verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt.&lt;br /&gt;
Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat,&lt;br /&gt;
umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierung==&lt;br /&gt;
&lt;br /&gt;
Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt&#039;&#039;&#039; - den im&lt;br /&gt;
Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist&lt;br /&gt;
nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift &#039;&#039;&#039;vor&#039;&#039;&#039; der&lt;br /&gt;
Adressauflösung und braucht deshalb weder eine Zeile in &#039;&#039;RESTSRV_ENDPOINTS&#039;&#039;&lt;br /&gt;
noch eine Berechtigung in &#039;&#039;RESTSRV_ACCESS&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Unterschieden wird nach Verb und Token-Art:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ablauf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; ohne &#039;&#039;Authorization&#039;&#039; || Anmelden (Skript-Methode &#039;&#039;Authenticate&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Refresh-Token || Erneuern (Skript-Methode &#039;&#039;Refresh&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DELETE&#039;&#039; mit Bearer, Access-Token || Abmelden (kein Skript nötig)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Access-Token || &#039;&#039;&#039;401&#039;&#039;&#039; - unverändert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist&lt;br /&gt;
Absicht. Würde ein vorgelegtes Access-Token beim &#039;&#039;POST&#039;&#039; als Abmeldung gedeutet,&lt;br /&gt;
würde ein Client, der seine beiden Token verwechselt, den Anwender still&lt;br /&gt;
abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 1: Token-Bezug===&lt;br /&gt;
&lt;br /&gt;
Der Konsument ruft den JWT-Endpunkt des Zugangs auf, z.B.:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
 Body:   {&amp;quot;username&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;password&amp;quot;:&amp;quot;...&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Der Server führt das im Zugang hinterlegte &#039;&#039;&#039;Authentifizierungs-Skript&#039;&#039;&#039; aus (F7 in der Zugangs-Liste). Das Skript muss eine Prozedur &#039;&#039;Authenticate&#039;&#039; bereitstellen, die über den Writer schreibt:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;status&#039;&#039; = &#039;&#039;1&#039;&#039; bei Erfolg, &#039;&#039;9&#039;&#039; bei Misserfolg&lt;br /&gt;
* &#039;&#039;error&#039;&#039; = Fehlertext (bei status=9)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;, &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; für die JWT-Claims (optional)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt;&#039;&#039; für beliebige Custom-Claims (z.B. Mandant &#039;&#039;tenant&#039;&#039;, Rollen &#039;&#039;roles&#039;&#039;); im Folge-Skript lesbar&lt;br /&gt;
* &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; (optional) löst die Ausstellung eines Refresh-Tokens aus; &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; setzt dessen Lebensdauer in Minuten (Default 90 Tage)&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Die Lebensdauer eines Refresh-Tokens ist nach oben auf &#039;&#039;&#039;372 Tage&#039;&#039;&#039;&lt;br /&gt;
begrenzt. Ein mit &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; länger ausgestelltes Token wird bei der&lt;br /&gt;
Vorlage &#039;&#039;&#039;nicht mehr verifiziert&#039;&#039;&#039; und mit 401 abgewiesen - der Fehler fällt&lt;br /&gt;
also erst Monate später auf, beim ersten Erneuerungsversuch.}}&lt;br /&gt;
&lt;br /&gt;
Die Signatur lautet &#039;&#039;procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&#039;&#039; - Details unter [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]. Bei Erfolg signiert der Server einen Token (HS256, Issuer &#039;&#039;OBS REST-Server&#039;&#039;, Lebenszeit gemäß JWT-Exp) und antwortet:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:        &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;:  &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:    28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:   &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Zum Aufbau der Antwort:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;token&#039;&#039; und &#039;&#039;accessToken&#039;&#039; enthalten &#039;&#039;&#039;denselben&#039;&#039;&#039; Token. &#039;&#039;accessToken&#039;&#039; ist der Name, den die meisten Client-Bibliotheken erwarten; &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten.&lt;br /&gt;
* &#039;&#039;expiresIn&#039;&#039; ist die Gültigkeit in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (JWT-Exp × 60).&lt;br /&gt;
* &#039;&#039;&#039;Alle weiteren Felder, die das Authenticate-Skript zurückgibt, werden mit ausgeliefert&#039;&#039;&#039; - mit Ausnahme von &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern. So kann das Skript z.B. eine Benutzer-Id, den Mandanten oder Rollen direkt in die Anmelde-Antwort legen, ohne dass der Client dafür einen zweiten Aufruf braucht.&lt;br /&gt;
&lt;br /&gt;
Beispiel mit Zusatzfeldern aus dem Skript:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;refreshToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;: 28800, &amp;quot;serverTime&amp;quot;: &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Mit der Ausstellung legt der Server zu dem Token-Paar eine &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; in&lt;br /&gt;
&#039;&#039;RESTSRV_TOKEN&#039;&#039; an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt,&lt;br /&gt;
Datenbankproblem), wird &#039;&#039;&#039;kein Token ausgeliefert&#039;&#039;&#039;: der Server antwortet mit&lt;br /&gt;
&#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Das ist bewusst von der abgelehnten Anmeldung&lt;br /&gt;
unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt&lt;br /&gt;
werden.&lt;br /&gt;
&lt;br /&gt;
Wird die Anmeldung abgelehnt (&#039;&#039;status&#039;&#039; = 9), antwortet der Server mit&lt;br /&gt;
&#039;&#039;&#039;401 Unauthorized&#039;&#039;&#039;, dem Code &#039;&#039;AUTH_FAILED&#039;&#039; und reicht den Text aus dem Feld&lt;br /&gt;
&#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch - der Konsument kann ihn also&lt;br /&gt;
direkt anzeigen. Die Formulierung liegt damit beim Skript.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;AUTH_FAILED&#039;&#039; unterscheidet die &#039;&#039;&#039;abgelehnte Anmeldung&#039;&#039;&#039; vom&lt;br /&gt;
&#039;&#039;AUTH_EXPIRED&#039;&#039;, das ein abgelaufenes oder gesperrtes Token liefert. Ein Client&lt;br /&gt;
sollte auf den Code reagieren und nicht auf den Text: Bei &#039;&#039;AUTH_FAILED&#039;&#039; gehört&lt;br /&gt;
die Anmeldemaske noch einmal vorgelegt, bei &#039;&#039;AUTH_EXPIRED&#039;&#039; erst ein Refresh&lt;br /&gt;
versucht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der durchgereichte Text ist für den Endanwender gedacht und sollte&lt;br /&gt;
keine internen Details verraten. „Benutzer oder Passwort ist falsch&amp;quot; ist eine gute&lt;br /&gt;
Meldung, „Kein Datensatz in BENUTZ mit b_name=meier&amp;quot; nicht.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 2: Token-Verwendung===&lt;br /&gt;
&lt;br /&gt;
Bei allen folgenden Aufrufen sendet der Client den Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 GET https://api.meinserver.de/kalender/v1&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi...&lt;br /&gt;
&lt;br /&gt;
Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die &#039;&#039;&#039;Sitzung&#039;&#039;&#039; in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Im Skript stehen die Claims als Parameter &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039; und &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; zur Verfügung.&lt;br /&gt;
&lt;br /&gt;
Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen&lt;br /&gt;
Sitzung gesperrt wurde - beides mit &#039;&#039;&#039;401&#039;&#039;&#039; und dem Code &#039;&#039;AUTH_EXPIRED&#039;&#039;. Der&lt;br /&gt;
Grund steht jeweils im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; unter &#039;&#039;TokenStore: ...&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Token, das &#039;&#039;&#039;vor&#039;&#039;&#039; der Einführung dieser Prüfung ausgestellt&lt;br /&gt;
wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich&lt;br /&gt;
angemeldete Konsumenten also einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 3: Token erneuern (Refresh)===&lt;br /&gt;
&lt;br /&gt;
Soll die App lange ohne Neuanmeldung arbeiten, stellt das Authenticate-Skript zusätzlich ein Refresh-Token aus (Feld &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039;). Zum Erneuern ruft der Client &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; auf, aber mit dem Refresh-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)&lt;br /&gt;
&lt;br /&gt;
Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;), &#039;&#039;&#039;entwertet es in der Sitzung&#039;&#039;&#039; und ruft erst dann im selben Skript die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar &#039;&#039;{&amp;quot;token&amp;quot;,&amp;quot;accessToken&amp;quot;,&amp;quot;refreshToken&amp;quot;,&amp;quot;expiresIn&amp;quot;,&amp;quot;serverTime&amp;quot;}&#039;&#039; in &#039;&#039;&#039;derselben Sitzung&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im&lt;br /&gt;
Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die&lt;br /&gt;
Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Verhalten bei einem bereits benutzten Refresh-Token:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Lage !! Antwort !! Wirkung auf die Sitzung&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;innerhalb von 60 Sekunden&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;später&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Die &#039;&#039;&#039;gesamte Sitzung wird gesperrt&#039;&#039;&#039;. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
===Phase 4: Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
Abgemeldet wird mit einem &#039;&#039;&#039;DELETE&#039;&#039;&#039; auf denselben JWT-Endpunkt, das&lt;br /&gt;
Access-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 DELETE https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Access-Token)&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 204 No Content&lt;br /&gt;
&lt;br /&gt;
Der Server sperrt die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039; - also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token aller vorherigen Erneuerungen. &#039;&#039;&#039;Ein Skript ist dafür&lt;br /&gt;
nicht nötig&#039;&#039;&#039;: es gibt keine Methode &#039;&#039;Logout&#039;&#039;, der Vorgang läuft komplett in&lt;br /&gt;
der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver&lt;br /&gt;
JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.&lt;br /&gt;
&lt;br /&gt;
* Ein wiederholter Aufruf ist unschädlich und bleibt &#039;&#039;&#039;204&#039;&#039;&#039; - auch wenn die Sitzung längst gesperrt ist.&lt;br /&gt;
* Ein abgelaufenes oder ungültiges Token ergibt &#039;&#039;&#039;401&#039;&#039;&#039; (es wird schon bei der Prüfung abgewiesen).&lt;br /&gt;
* Ein Refresh-Token statt des Access-Tokens ergibt &#039;&#039;&#039;401&#039;&#039;&#039;.&lt;br /&gt;
* Lässt sich die Sitzung nicht sperren, antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039; - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
====Alle Geräte abmelden====&lt;br /&gt;
&lt;br /&gt;
Der Fall „Gerät verloren&amp;quot; ist bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; am JWT-Endpunkt untergebracht:&lt;br /&gt;
er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das&lt;br /&gt;
&#039;&#039;oWriter.JwtRevoke&#039;&#039;, das jeder gewöhnliche Endpunkt aufrufen kann:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // &#039;session&#039; = nur diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;all&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;all&#039;&#039; setzt voraus, dass das Authenticate-Skript &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;&lt;br /&gt;
gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und&lt;br /&gt;
der Aufruf antwortet mit 503.}}&lt;br /&gt;
&lt;br /&gt;
==Woher ein Anmelder seine Identität bekommt==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Diese Frage entscheidet das Skript des Zugangs, nicht der Server.&#039;&#039;&#039; Das&lt;br /&gt;
Authentifizierungs-Skript liegt je Zugang in &amp;lt;code&amp;gt;ra_jwt_script&amp;lt;/code&amp;gt; - und&lt;br /&gt;
damit gilt für jede Anwendung ihre eigene Regel.&lt;br /&gt;
&lt;br /&gt;
Das ist keine Kleinigkeit, sondern der Punkt, an dem sich zwei Anwendungen&lt;br /&gt;
unterscheiden. Ein Beispiel aus dem Betrieb: Ein Zugang für eine&lt;br /&gt;
Techniker-Anwendung lehnt jeden Benutzer ohne Mitarbeiterzuordnung&lt;br /&gt;
(&amp;lt;code&amp;gt;BENUTZ.b_mitarbeiter&amp;lt;/code&amp;gt;) ab - &#039;&#039;&#039;bewusst&#039;&#039;&#039;, denn ein Techniker ohne&lt;br /&gt;
Zuordnung bekäme eine leere Auftragsliste und würde den Fehler bei sich suchen.&lt;br /&gt;
Für einen Kundenzugang wäre genau dieselbe Regel falsch: sie träfe &#039;&#039;&#039;jeden&#039;&#039;&#039;&lt;br /&gt;
Anmelder.&lt;br /&gt;
&lt;br /&gt;
Wer einen zweiten Zugang für eine andere Anwendung anlegt, prüft deshalb als&lt;br /&gt;
Erstes: Woraus besteht die Identität dieser Anmelder, und welche Ablehnung ist&lt;br /&gt;
für sie richtig? Ein kopiertes Skript bringt die Regel der anderen Anwendung&lt;br /&gt;
mit, und sie fällt erst auf, wenn sich der erste echte Anmelder nicht anmelden&lt;br /&gt;
kann.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen (RESTSRV_TOKEN)==&lt;br /&gt;
&lt;br /&gt;
Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die&lt;br /&gt;
Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung&lt;br /&gt;
sowie die Kennzeichen „verbraucht&amp;quot; und „gesperrt&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rotation und Widerruf sind getrennt.&#039;&#039;&#039; Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.&lt;br /&gt;
* &#039;&#039;&#039;Die Sitzung überlebt die Rotation.&#039;&#039;&#039; Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.&lt;br /&gt;
* &#039;&#039;&#039;Aufräumen läuft automatisch&#039;&#039;&#039; beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.&lt;br /&gt;
* &#039;&#039;&#039;Kein Eingriff nötig.&#039;&#039;&#039; Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als &#039;&#039;TokenStore: ...&#039;&#039; in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==mTLS-Subject-Prüfung==&lt;br /&gt;
&lt;br /&gt;
Wird im Feld &#039;&#039;&#039;mTLS-Subject&#039;&#039;&#039; ein Wert hinterlegt, prüft der Server zusätzlich zur Standard-mTLS-Validierung, ob der Subject-DN des (CA-validierten) Client-Zertifikats die hinterlegten &#039;&#039;&#039;RDN-Komponenten&#039;&#039;&#039; enthält. Verglichen wird jede Komponente &#039;&#039;&#039;vollständig&#039;&#039;&#039; (kein Substring-Match) und case-insensitiv; mehrere Komponenten werden durch Komma oder Semikolon getrennt und müssen &#039;&#039;&#039;alle&#039;&#039;&#039; im Subject vorkommen.&lt;br /&gt;
&lt;br /&gt;
Beispiele für Subject-Strings im Cert (OneLine-DN):&lt;br /&gt;
&lt;br /&gt;
 /C=DE/O=Kunde GmbH/CN=client-prod.kunde.de&lt;br /&gt;
 /C=DE/O=Kunde GmbH/CN=client-test.kunde.de&lt;br /&gt;
&lt;br /&gt;
Mit &#039;&#039;&#039;CN=client-prod.kunde.de&#039;&#039;&#039; im Feld wird genau das prod-Cert akzeptiert, das test-Cert mit 401 abgewiesen. Da jede RDN-Komponente vollständig verglichen wird, matcht z.B. &#039;&#039;&#039;CN=client1&#039;&#039;&#039; nicht fälschlich auch &#039;&#039;&#039;CN=client10&#039;&#039;&#039;. Mehrere Bedingungen lassen sich mit Komma/Semikolon kombinieren, z.B. &#039;&#039;O=Kunde GmbH;CN=client-prod.kunde.de&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Zugänge-Liste (allein):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neuer Zugang&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - Zugang bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung&lt;br /&gt;
* &#039;&#039;&#039;F7&#039;&#039;&#039; - JWT-Authentifizierungs-Skript bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F8&#039;&#039;&#039; - Statistik für den Zugang&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste via F6 geöffnet (Auswahl für Berechtigung):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;F2&#039;&#039;&#039; - markierte Zugänge dem Endpunkt zuordnen&lt;br /&gt;
* &#039;&#039;&#039;F5&#039;&#039;&#039; - Zugang in der Liste markieren / Markierung aufheben&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=65545</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=65545"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: HSTS-Header und Abgrenzung der Verbindungsgrenzen ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| Info                || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| Kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS                || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Cert                || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Key                 || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Cert           || PEM-Inhalt des CA-Root-Zertifikats (Pflicht bei Standard-TLS, dort für Chain-Validierung, sowie bei mTLS, dort zusätzlich für Client-Cert-Verifikation)&lt;br /&gt;
|-&lt;br /&gt;
| Limit-Fenster       || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Limit Erfolg        || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Limit Fehler        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Limit gesamt        || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| Max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| Max. parallel       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| Keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;Max. Verbindungen&#039;&#039; und &#039;&#039;Max. parallel&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Cert + Key + Root-Cert hinterlegt     || Empfohlener Default für alle Server, die über das öffentliche Netz erreichbar sind&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Cert + Key + Root-Cert (CA)           || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;Kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Cert/Key gar nicht erst hoch. Bei Standard-TLS ohne Root-Cert wird die Anlage abgelehnt - die Chain-Validierung benötigt das CA-Root. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Ein Zertifikatswechsel erfolgt durch ändern der PEM-Inhalte in der DB und einen &#039;&#039;&#039;Neustart&#039;&#039;&#039; des REST-Dienstes (kein Hot-Reload). Bis zum Neustart läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus.&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Beim Speichern wird geprüft:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name müssen vergeben sein.&lt;br /&gt;
* Nr muss eindeutig sein.&lt;br /&gt;
* Name muss eindeutig sein.&lt;br /&gt;
* Bei nicht aktiviertem &#039;&#039;Kein SSL&#039;&#039; müssen Cert und Key vorhanden sein.&lt;br /&gt;
* Bei nicht aktiviertem &#039;&#039;mTLS&#039;&#039; (= Standard-TLS) muss Root-Cert vorhanden sein.&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=65544</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=65544"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Wartung der Idempotenz-Tabelle berichtigt, Stolperstein fehlender Idempotency-Key ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, ggf. mit Chain), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten.&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat + Key + Root-Zertifikat hinterlegen.&lt;br /&gt;
** &#039;&#039;&#039;mTLS&#039;&#039;&#039; aktivieren: zusätzlich CA-Root-Zertifikat hinterlegen, jeder Client muss ein gültiges Cert vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;Kein SSL&#039;&#039;&#039; aktivieren: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Scripting&amp;diff=65543</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Scripting&amp;diff=65543"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Idempotenz bei 5xx berichtigt (Schluessel wird freigegeben), Pflichtheader und Dateiantwort ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Anleitung für Endpunkt-Skripte=&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
==Sicherheitshinweise==&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über &#039;&#039;DB_SQLVal&#039;&#039; bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.}}&lt;br /&gt;
&lt;br /&gt;
* Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).&lt;br /&gt;
* Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.&lt;br /&gt;
* Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Methoden-Signatur==&lt;br /&gt;
&lt;br /&gt;
Pro HTTP-Methode wird im Skript eine gleichnamige &#039;&#039;&#039;Prozedur&#039;&#039;&#039; implementiert. Der Server ruft genau die Prozedur auf, die zur Methode des eingehenden Requests passt.&lt;br /&gt;
&lt;br /&gt;
 procedure Get   (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Post  (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Put   (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Patch (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
&lt;br /&gt;
Zwei Handles, kein Rückgabewert: &#039;&#039;&#039;oReader&#039;&#039;&#039; ist die Anfrage, &#039;&#039;&#039;oWriter&#039;&#039;&#039; die&lt;br /&gt;
Antwort. Beide erzeugt der Server, beide gehören ihm - im Skript wird nichts&lt;br /&gt;
angelegt und nichts freigegeben.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;DELETE hat keine Skript-Methode.&#039;&#039;&#039; &amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt; ist in der&lt;br /&gt;
Skriptsprache eine Standardprozedur (Löschen aus einer Zeichenkette); eine eigene&lt;br /&gt;
Prozedur dieses Namens lässt sich nicht übersetzen, und der Fehler nennt nur die&lt;br /&gt;
Zeile der Deklaration. Wer einen DELETE-Endpunkt braucht, legt die Methode unter&lt;br /&gt;
einem anderen Verb ab oder trennt sie in ein eigenes Skript.}}&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Funktion, antwortet der Server mit &#039;&#039;&#039;405 Method&lt;br /&gt;
Not Allowed&#039;&#039;&#039; (&amp;lt;code&amp;gt;METHOD_NOT_ALLOWED&amp;lt;/code&amp;gt;) und nennt im Header&lt;br /&gt;
&amp;lt;code&amp;gt;Allow&amp;lt;/code&amp;gt; die Verben, die dieses Skript tatsächlich anbietet - die Liste&lt;br /&gt;
stammt aus dem kompilierten Skript und kann deshalb nicht veralten:&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 405 Method Not Allowed&lt;br /&gt;
 Allow: GET, PUT&lt;br /&gt;
&lt;br /&gt;
Ein im Vertrag zugesagtes Verb braucht also seine Prozedur. Fehlt sie, ist das an&lt;br /&gt;
der Antwort sofort zu erkennen und &#039;&#039;&#039;kein&#039;&#039;&#039; Serverfehler.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Die Anfrage lesen: oReader==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;oReader&#039;&#039; ist der einzige Zugang zur Anfrage - Query, Header, Formularfelder,&lt;br /&gt;
Pfadwerte, Token-Claims und Körper. Alles davon sind &#039;&#039;&#039;Zeichenketten&#039;&#039;&#039; oder&lt;br /&gt;
werden über einen typisierten Leser geholt.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Was er liefert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;kundennr&#039;)&amp;lt;/code&amp;gt; || Query-Parameter, POST-Parameter (form-urlencoded) oder HTTP-Header. Getrimmt, leer wenn nicht gesendet&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt; || Wert eines Platzhalters aus dem Pfad-Template&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Form(&#039;bemerkung&#039;)&amp;lt;/code&amp;gt; || Formularfeld einer &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt;-Übertragung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Claim(&#039;tenant&#039;)&amp;lt;/code&amp;gt; || Custom-Claim des vorgelegten Tokens&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Subject()&amp;lt;/code&amp;gt; || Subject des Tokens (&#039;&#039;sub&#039;&#039;-Claim)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.TraceId()&amp;lt;/code&amp;gt; || Korrelations-ID des Requests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || UTC-Offset des Servers, z.B. &#039;&#039;+02:00&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.HasBody()&amp;lt;/code&amp;gt; || Wurde überhaupt ein JSON-Körper gesendet?&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Filterung durch den Server:&lt;br /&gt;
&lt;br /&gt;
* Geblockte Header werden nicht durchgereicht: &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039;.&lt;br /&gt;
* Werte, die von aussen kommen, können die reservierten Namen nicht besetzen.&lt;br /&gt;
* Werte werden auf max. 1024 Zeichen begrenzt.&lt;br /&gt;
* Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.&lt;br /&gt;
&lt;br /&gt;
Zugriff im Skript:&lt;br /&gt;
&lt;br /&gt;
 cKundenNr := oReader.Param(&#039;kundennr&#039;);&lt;br /&gt;
 if (not Empty(cKundenNr)) then begin&lt;br /&gt;
     // ...&lt;br /&gt;
 end;&lt;br /&gt;
&lt;br /&gt;
===Der Körper: Zugriff über Pfade===&lt;br /&gt;
&lt;br /&gt;
Den JSON-Körper liest &#039;&#039;oReader&#039;&#039; über &#039;&#039;&#039;Pfade&#039;&#039;&#039;. Es gibt keine Unterobjekte im&lt;br /&gt;
Skript, keinen Cast, kein &#039;&#039;.Items[i]&#039;&#039; - und damit auch keine Lebensdauer, um die&lt;br /&gt;
sich jemand kümmern müsste.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Str(&#039;betreff&#039;, cWert)&amp;lt;/code&amp;gt; || Liest den Wert. &#039;&#039;&#039;Rückgabe Boolean&#039;&#039;&#039;: war das Feld da? Fehlt es, bleibt die Variable unverändert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Int(&#039;menge&#039;, nWert)&amp;lt;/code&amp;gt; || dito, Ganzzahl&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Num(&#039;preis&#039;, nWert)&amp;lt;/code&amp;gt; || dito, Kommazahl. &#039;&#039;&#039;Der Dezimalpunkt aus JSON wird richtig gelesen&#039;&#039;&#039; - kein &#039;&#039;StrTran&#039;&#039; mehr&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Bool(&#039;aktiv&#039;, lWert)&amp;lt;/code&amp;gt; || dito, Boolean&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.NumDef(&#039;rabatt&#039;, nWert, 0)&amp;lt;/code&amp;gt; || wie &#039;&#039;Num&#039;&#039;, mit Vorgabewert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Has(&#039;feld&#039;)&amp;lt;/code&amp;gt; || Ist das Feld vorhanden? (auch bei &#039;&#039;null&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Count(&#039;zeiten&#039;)&amp;lt;/code&amp;gt; || Anzahl Einträge einer Liste, 0 wenn keine Liste&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.IsArr(&#039;zeiten&#039;)&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;IsObj(&#039;zeiten[0]&#039;)&amp;lt;/code&amp;gt; || Gestalt prüfen&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Keys()&amp;lt;/code&amp;gt; || Alle Feldnamen der obersten Ebene, in Kommas eingefasst&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Verschachtelung steht im Pfad&#039;&#039;&#039; - Punkt für Felder, eckige Klammern für Listen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cVon : string;&lt;br /&gt;
    nAnz : Integer;&lt;br /&gt;
    nI   : Integer;&lt;br /&gt;
begin&lt;br /&gt;
    nAnz := oReader.Count(&#039;zeiten&#039;);&lt;br /&gt;
    for nI := 0 to nAnz - 1 do begin&lt;br /&gt;
        if (oReader.Str(&#039;zeiten[&#039; + IntToStr(nI) + &#039;].von&#039;, cVon)) then begin&lt;br /&gt;
            // ... cVon verarbeiten ...&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Der Rückgabewert unterscheidet „nicht gesendet&amp;quot; von „leer&lt;br /&gt;
gesendet&amp;quot;.&#039;&#039;&#039; &amp;lt;code&amp;gt;oReader.Str(&#039;feld&#039;, cWert)&amp;lt;/code&amp;gt; liefert bei&lt;br /&gt;
&amp;lt;code&amp;gt;&amp;quot;feld&amp;quot;: null&amp;lt;/code&amp;gt; &#039;&#039;true&#039;&#039; mit leerem Wert und bei fehlendem Feld&lt;br /&gt;
&#039;&#039;false&#039;&#039;. Genau diese Unterscheidung braucht ein PATCH-artiger Aufruf: „Feld&lt;br /&gt;
löschen&amp;quot; und „Feld nicht anfassen&amp;quot; sehen sonst gleich aus.}}&lt;br /&gt;
&lt;br /&gt;
Ist gar kein Körper gesendet worden, liefert jeder Leser &#039;&#039;false&#039;&#039; und&lt;br /&gt;
&#039;&#039;HasBody()&#039;&#039; ist &#039;&#039;false&#039;&#039;. Maximale Body-Grösse: 10 MB - bei einem gepackt gesendeten Körper zusätzlich das 400fache der gesendeten Grösse, siehe [[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Kompression. Beide Überschreitungen beantwortet der Server mit &#039;&#039;&#039;413&#039;&#039;&#039;, das Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
===Werte des Servers===&lt;br /&gt;
&lt;br /&gt;
Bei aktiver JWT-Authentifizierung stehen die Token-Claims über den Reader zur&lt;br /&gt;
Verfügung. Sie tragen intern weiterhin die Namen mit Präfix &#039;&#039;&#039;_OBS_&#039;&#039;&#039; - im&lt;br /&gt;
Skript werden sie aber über die Zugriffe der Tabelle oben gelesen und nicht mehr&lt;br /&gt;
über den Namen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Parameter            !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_ID&#039;)&amp;lt;/code&amp;gt; || JWT-Id (&#039;&#039;jti&#039;&#039;-Claim) - typisch die User-Id. &#039;&#039;&#039;Muss nicht eindeutig sein&#039;&#039;&#039;: die Sitzung führt der Server über eigene Kennungen (siehe unten)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Subject()&amp;lt;/code&amp;gt; || Subject (&#039;&#039;sub&#039;&#039;-Claim) - typisch Benutzername&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_AUDIENCE&#039;)&amp;lt;/code&amp;gt; || Audience (&#039;&#039;aud&#039;&#039;-Claim) - typisch Mandant / Rolle&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Claim(&#039;tenant&#039;)&amp;lt;/code&amp;gt; || Beliebiger Custom-Claim des Tokens, hier &#039;&#039;tenant&#039;&#039;; im Folge-Skript lesbar&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.TraceId()&amp;lt;/code&amp;gt; || Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Unabhängig von JWT stehen ausserdem immer zur Verfügung:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Parameter            !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_SERVER_TIME&#039;)&amp;lt;/code&amp;gt; || Aktuelle Serverzeit als ISO-8601 &#039;&#039;&#039;mit UTC-Offset&#039;&#039;&#039; (z.B. &#039;&#039;2026-08-17T09:12:33+02:00&#039;&#039;). Nützlich, um Datumswerte in derselben Zeitzone auszuliefern, in der der Server arbeitet, und damit ein Client seinen Uhren-Versatz bestimmen kann&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || Nur der Offset daraus, z.B. &#039;&#039;+02:00&#039;&#039;. Er geht in die Zeitfunktionen (siehe [[#Zeitangaben auf der Leitung|Zeitangaben]])&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_EXP_SEC&#039;)&amp;lt;/code&amp;gt; || Laufzeit des Access-Tokens in Sekunden (&#039;&#039;JWT-Exp&#039;&#039; × 60). Damit kann ein Konfigurations-Endpunkt denselben Wert ausliefern, den der Token wirklich hat&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.&lt;br /&gt;
&lt;br /&gt;
Zusätzlich trägt jedes Token zwei &#039;&#039;&#039;Sitzungskennungen des Servers&#039;&#039;&#039;, lesbar als&lt;br /&gt;
&#039;&#039;oReader.Claim(&#039;sid&#039;)&#039;&#039; und &#039;&#039;oReader.Claim(&#039;rid&#039;)&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Claim !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| sid || Sitzung. Bleibt über alle Erneuerungen (Refresh) hinweg gleich und ist der Schlüssel für das Abmelden&lt;br /&gt;
|-&lt;br /&gt;
| rid || Zeilenkennung des einzelnen Token-Paares in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Bei jeder Erneuerung neu&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide sind OBS-UIDs (10 Zeichen) und werden vom Server &#039;&#039;&#039;nach&#039;&#039;&#039; den Claims des&lt;br /&gt;
Skripts gesetzt - ein Skript kann sie also auch mit einem eigenen Claim &#039;&#039;sid&#039;&#039;&lt;br /&gt;
nicht überschreiben. Für Fachlogik sind sie nicht gedacht; sie sind nützlich, um eine&lt;br /&gt;
Sitzung im Protokoll wiederzufinden.&lt;br /&gt;
&lt;br /&gt;
===Pfad-Parameter===&lt;br /&gt;
&lt;br /&gt;
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), stehen die aus den Platzhaltern erfassten Werte über &#039;&#039;oReader.Path&#039;&#039; bereit:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Template !! Zugriff im Skript&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;oReader.Path(&#039;code&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Diese Werte kommen aus dem Routing und sind durch den Client nicht fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].&lt;br /&gt;
&lt;br /&gt;
===Datei-Uploads===&lt;br /&gt;
&lt;br /&gt;
Ist der Endpunkt für Uploads freigeschaltet (&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;, siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server&lt;br /&gt;
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und&lt;br /&gt;
übergibt dem Skript Pfad, Originalname und Content-Type über den Reader. Das&lt;br /&gt;
Skript entscheidet selbst über die weitere Verarbeitung (z.B. DMS-Ablage). Der&lt;br /&gt;
Body wird in diesem Fall &#039;&#039;&#039;nicht&#039;&#039;&#039; als JSON geparst - &#039;&#039;oReader.HasBody()&#039;&#039; ist&lt;br /&gt;
&#039;&#039;false&#039;&#039;. Es gilt nicht das JSON-Body-Limit (10&amp;amp;nbsp;MB), sondern die pro&lt;br /&gt;
Endpunkt konfigurierte Grösse (&#039;&#039;re_upload_size&#039;&#039;, Standard 25&amp;amp;nbsp;MB;&lt;br /&gt;
Überschreitung -&amp;gt; 413).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Wie die Datei übertragen wurde, spielt für das Skript keine Rolle.&#039;&#039;&#039; Beide&lt;br /&gt;
Wege - &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; und der resumable&lt;br /&gt;
&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;-Upload - liefern dieselben vier Werte. Das&lt;br /&gt;
Übertragungsprotokoll samt Header, Statuscodes und Resume-Verhalten steht in&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt; || Vollständiger Pfad zur temporären Datei auf dem Server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadName()&amp;lt;/code&amp;gt; || Originaldateiname&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadType()&amp;lt;/code&amp;gt; || Content-Type der Datei (Default &amp;lt;code&amp;gt;application/octet-stream&amp;lt;/code&amp;gt;, wenn der Client keinen angibt), kleingeschrieben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadSha()&amp;lt;/code&amp;gt; || SHA-256 der &#039;&#039;&#039;gespeicherten&#039;&#039;&#039; Datei, hex in Kleinbuchstaben. Damit lässt sich eine vom Client mitgesendete Prüfsumme vergleichen - erst dieser Vergleich macht aus ihr eine Zusicherung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der &#039;&#039;&#039;Dateiname ist Pflicht&#039;&#039;&#039;. Fehlt er, lehnt der Server den Upload mit&lt;br /&gt;
&#039;&#039;&#039;400 Bad Request&#039;&#039;&#039; ab und das Skript wird nicht ausgeführt - das Skript kann&lt;br /&gt;
sich also darauf verlassen, dass &#039;&#039;UploadPath()&#039;&#039; und &#039;&#039;UploadName()&#039;&#039; gefüllt&lt;br /&gt;
sind, sobald es läuft.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}}&lt;br /&gt;
&lt;br /&gt;
Zusatzangaben, die im selben Aufruf mitkommen, liest das Skript mit&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt; - bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; sind&lt;br /&gt;
das die Partien ohne &amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt;. Ein vollständiges Beispiel zeigt&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]].&lt;br /&gt;
&lt;br /&gt;
==Die Antwort schreiben: oWriter==&lt;br /&gt;
&lt;br /&gt;
Der Writer schreibt &#039;&#039;&#039;sequentiell&#039;&#039;&#039;: jeder Aufruf hängt ein Feld an, an der&lt;br /&gt;
Stelle, an der er steht. Die äussere Klammer der Antwort setzt der Server. Wird&lt;br /&gt;
nichts geschrieben, antwortet er mit &#039;&#039;{}&#039;&#039;. Content-Type ist&lt;br /&gt;
&#039;&#039;application/json; charset=utf-8&#039;&#039;, der Status 200, sofern das Skript nichts&lt;br /&gt;
anderes setzt.&lt;br /&gt;
&lt;br /&gt;
Einfaches Beispiel:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str(&#039;wert_string&#039;, &#039;123&#039;);&lt;br /&gt;
    oWriter.Int(&#039;wert_int&#039;   , 456);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ergebnis&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Str(&#039;name&#039;, cWert)&amp;lt;/code&amp;gt; || Zeichenkette, immer maskiert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.StrN(&#039;name&#039;, cWert)&amp;lt;/code&amp;gt; || wie &#039;&#039;Str&#039;&#039;, aber &#039;&#039;&#039;null&#039;&#039;&#039; statt einer leeren Zeichenkette&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Int(&#039;anz&#039;, nWert)&amp;lt;/code&amp;gt; || Ganzzahl&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Num(&#039;preis&#039;, nWert, 2)&amp;lt;/code&amp;gt; || Kommazahl mit fester Stellenzahl. &#039;&#039;&#039;Immer mit Dezimalpunkt&#039;&#039;&#039;, unabhängig von der Locale des Servers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Bool(&#039;aktiv&#039;, lWert)&amp;lt;/code&amp;gt; || &#039;&#039;true&#039;&#039; / &#039;&#039;false&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Null(&#039;lat&#039;)&amp;lt;/code&amp;gt; || ausdrücklich &#039;&#039;null&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.ObjBegin(&#039;kunde&#039;)&amp;lt;/code&amp;gt; … &amp;lt;code&amp;gt;oWriter.ObjEnd&amp;lt;/code&amp;gt; || Unterobjekt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.ArrBegin(&#039;dateien&#039;)&amp;lt;/code&amp;gt; … &amp;lt;code&amp;gt;oWriter.ArrEnd&amp;lt;/code&amp;gt; || Liste&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.RootArr()&amp;lt;/code&amp;gt; || Die &#039;&#039;&#039;Wurzel&#039;&#039;&#039; ist eine Liste statt eines Objekts. Muss vor dem ersten Feld stehen; ein &#039;&#039;ArrEnd&#039;&#039; gibt es dazu nicht&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Raw(&#039;meta&#039;, cJson&#039;)&amp;lt;/code&amp;gt; || Fertiges JSON-Fragment einbetten. Siehe unten&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;In einer Liste hat ein Eintrag keinen Namen&#039;&#039;&#039; - der leere Feldname ist genau&lt;br /&gt;
das:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
oWriter.ArrBegin(&#039;dateien&#039;);&lt;br /&gt;
    while (not q.EoF) do begin&lt;br /&gt;
        oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
            oWriter.Str (&#039;uid&#039;      , q.A2C(&#039;f_uid&#039;));&lt;br /&gt;
            oWriter.Int (&#039;bytes&#039;    , q.A2I(&#039;f_bytes&#039;));&lt;br /&gt;
            oWriter.StrN(&#039;kategorie&#039;, AllTrim(q.A2C(&#039;f_kat&#039;)));   // null wenn leer&lt;br /&gt;
        oWriter.ObjEnd;&lt;br /&gt;
        q.Next();&lt;br /&gt;
    end;&lt;br /&gt;
oWriter.ArrEnd;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ein Name in einer Liste und ein fehlender Name in einem Objekt sind beides&lt;br /&gt;
&#039;&#039;&#039;Strukturfehler&#039;&#039;&#039;. Der Server antwortet dann mit &#039;&#039;&#039;500&#039;&#039;&#039; und nennt die&lt;br /&gt;
Stelle im Protokoll - er liefert kein halbes JSON aus. Dasselbe gilt für einen&lt;br /&gt;
Stapel, der am Ende der Methode noch offen ist.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Ein Helfer bekommt den Writer als Parameter&#039;&#039;&#039; und schreibt an der&lt;br /&gt;
Stelle hinein, an der er aufgerufen wird - er gibt kein Fragment zurück. Damit&lt;br /&gt;
steht die Reihenfolge der Antwort im Code und nicht im Zusammenbau:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;procedure DateienSchreiben(oWriter: TxRestWriter; const cNr: string);&amp;lt;/code&amp;gt;}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Raw ist der Notausgang und prüft.&#039;&#039;&#039; Er parst das Fragment und bettet den&lt;br /&gt;
&#039;&#039;&#039;Originaltext&#039;&#039;&#039; ein; ungültiges JSON gibt einen Serverfehler, der das Feld&lt;br /&gt;
nennt. Gebraucht wird er für ein gespeichertes, &#039;&#039;&#039;signiertes&#039;&#039;&#039; Dokument:&lt;br /&gt;
Parsen und Neuserialisieren würde die signierte Feldreihenfolge zerstören.&lt;br /&gt;
&lt;br /&gt;
===Zeitangaben auf der Leitung===&lt;br /&gt;
&lt;br /&gt;
Zeitstempel gehen als &#039;&#039;&#039;ISO-8601 mit Offset&#039;&#039;&#039; über die Leitung. Die Wandlung&lt;br /&gt;
gehört nicht ins Skript - dort saß der Fehler „zwei Stunden zu früh&amp;quot; an drei&lt;br /&gt;
Stellen gleichzeitig:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestIsoFromDate(dWert, cOffset)&amp;lt;/code&amp;gt; || OBS-Zeitpunkt -&amp;gt; &#039;&#039;2026-09-09T14:12:00+02:00&#039;&#039;. Leerer String bei leerem Datum&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestDateFromIso(cWert, cOffset)&amp;lt;/code&amp;gt; || ISO-Zeichenkette -&amp;gt; OBS-Zeitpunkt in &#039;&#039;&#039;Ortszeit des Servers&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || Der Offset, der in beide hineingeht&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestOffsetToMinutes(cOffset)&amp;lt;/code&amp;gt; || Offset in Minuten, für eigene Rechnungen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Antwort steuern: Statuscode, Header, traceId==&lt;br /&gt;
&lt;br /&gt;
Statuscode, Header und das Sperren einer Sitzung laufen über eigene Aufrufe des&lt;br /&gt;
Writers, &#039;&#039;&#039;nicht&#039;&#039;&#039; über Felder im Körper.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Status(201)&amp;lt;/code&amp;gt; || HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Header(&#039;ETag&#039;, &#039;7&#039;)&amp;lt;/code&amp;gt; || Beliebiger Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen und Werten werden entfernt (Schutz vor Header-Injection)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.NoBody()&amp;lt;/code&amp;gt; || Kein Körper - für &#039;&#039;&#039;204&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.JwtRevoke(&#039;session&#039;)&amp;lt;/code&amp;gt; || Sperrt die Sitzung des vorgelegten Tokens (&#039;&#039;session&#039;&#039;) oder alle Sitzungen des Subjects (&#039;&#039;all&#039;&#039;). Damit wird ein Endpunkt zum Logout, ohne eigene Verwaltung. Scheitert das Sperren, antwortet der Server mit &#039;&#039;&#039;503&#039;&#039;&#039; statt mit der Antwort des Skripts - siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Abschnitt Abmelden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld „menge&amp;quot; fehlt&#039;)&amp;lt;/code&amp;gt; || Fertige Fehlerantwort in der Form des Servers, siehe [[#Fehlerbehandlung im Skript|Fehlerbehandlung]]&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.HasError()&amp;lt;/code&amp;gt; || Steht schon eine Fehlerantwort im Writer? Für einen Verteiler, der danach noch etwas anhängen will&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.SendFile(cPfad, cName, cTyp)&amp;lt;/code&amp;gt; || Statt eines JSON-Körpers eine &#039;&#039;&#039;Datei&#039;&#039;&#039; ausliefern. Die Datei bleibt liegen, der Download ist wiederaufsetzbar. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]]&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.SendTempFile(cPfad, cName, cTyp)&amp;lt;/code&amp;gt; || Wie &amp;lt;code&amp;gt;SendFile&amp;lt;/code&amp;gt;, aber die Datei wird nach dem Senden &#039;&#039;&#039;gelöscht&#039;&#039;&#039; und der Download ist nicht wiederaufsetzbar - für Ergebnisse, die je Abruf entstehen (CSV-Export, Report)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Zusätzlich trägt jede Antwort den Header &#039;&#039;&#039;X-Trace-Id&#039;&#039;&#039; (Korrelations-ID). Dieselbe ID liegt dem Skript als &#039;&#039;oReader.TraceId()&#039;&#039; vor und erscheint in jeder Server-Fehler-Logzeile in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; - so lässt sich ein Fehler ohne Gerätezugriff im Log wiederfinden.&lt;br /&gt;
&lt;br /&gt;
Setzt ein Skript den Header &#039;&#039;&#039;Content-Encoding&#039;&#039;&#039; selbst - etwa weil es einen bereits gepackten Körper ausliefert -, hält sich die Engine heraus und packt die Antwort nicht noch einmal. Ohne diesen Header entscheidet der Server selbst, ob gzip zum Einsatz kommt.&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Anlegen mit 201, Location und ETag===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cNeueUid: string;&lt;br /&gt;
begin&lt;br /&gt;
    // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;uid&#039;, cNeueUid);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
    oWriter.Header(&#039;Location&#039;, &#039;/v1/orders/&#039; + cNeueUid);&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;    , &#039;1&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Strukturierter Fehler mit Statuscode und traceId===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var nMenge: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    if (not oReader.Int(&#039;menge&#039;, nMenge)) then begin&lt;br /&gt;
        // Statuscode, code, message, uid und traceId in EINEM Aufruf - dieselbe&lt;br /&gt;
        // Huelle, die auch die Server-eigenen Fehler tragen.&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;menge&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Optimistic Concurrency (ETag / If-Match)===&lt;br /&gt;
&lt;br /&gt;
Mit Statuscode, Headern und dem lesbaren Header &#039;&#039;If-Match&#039;&#039; führt das Skript je Datensatz einen Versionszähler und lehnt veraltete Schreibzugriffe mit 409 ab:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var nAktuell: Integer;&lt;br /&gt;
    nIfMatch: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    nAktuell := AuftragVersion(oReader.Path(&#039;uid&#039;));&lt;br /&gt;
    nIfMatch := iVal(oReader.Param(&#039;if-match&#039;));&lt;br /&gt;
&lt;br /&gt;
    if (nIfMatch &amp;lt;&amp;gt; nAktuell) then begin&lt;br /&gt;
        // Der ETag gehoert AUCH an die Ablehnung - sonst kennt der Client den&lt;br /&gt;
        // gueltigen Wert nicht und sein naechster Versuch scheitert wieder.&lt;br /&gt;
        oWriter.Header(&#039;ETag&#039;, xStr(nAktuell));&lt;br /&gt;
        oWriter.Error(409, &#039;VERSION_CONFLICT&#039;, &#039;Der Datensatz wurde zwischenzeitlich geaendert&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // ... speichern, Version hochzählen ...&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;, xStr(nAktuell + 1));&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Idempotenz - was das Skript beachten muss==&lt;br /&gt;
&lt;br /&gt;
Bei &#039;&#039;POST&#039;&#039;/&#039;&#039;PUT&#039;&#039;/&#039;&#039;PATCH&#039;&#039;/&#039;&#039;DELETE&#039;&#039; fängt der &#039;&#039;&#039;Server&#039;&#039;&#039; doppelte&lt;br /&gt;
Sendungen über den Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ab. Das Skript braucht dafür&lt;br /&gt;
&#039;&#039;&#039;keine eigene Logik&#039;&#039;&#039; - keine Schlüssel-Tabelle, keine Prüfung am Anfang der&lt;br /&gt;
Methode.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Header ist bei diesen vier Verben &#039;&#039;&#039;Pflicht&#039;&#039;&#039;. Fehlt er, antwortet&lt;br /&gt;
der Server mit &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; - das Skript läuft&lt;br /&gt;
dann gar nicht erst an, es muss den Fall also auch nicht behandeln.}}&lt;br /&gt;
&lt;br /&gt;
Was das Skript wissen muss:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Antwort des Skripts !! Wirkung auf den Idempotenz-Speicher&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;2xx&#039;&#039;&#039; || Statuscode, Body und Header werden gespeichert. Eine Wiederholung mit demselben Schlüssel bekommt genau diese Antwort zurück, das Skript läuft nicht erneut&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;2xx&#039;&#039;&#039; mit &#039;&#039;SendFile&#039;&#039;/&#039;&#039;SendTempFile&#039;&#039; || &#039;&#039;&#039;Nichts&#039;&#039;&#039; wird gespeichert - eine Dateiantwort liesse sich nicht wiedergeben. Der Schlüssel wird freigegeben und ist danach wieder benutzbar&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;4xx&#039;&#039;&#039; || Der Schlüssel wird &#039;&#039;&#039;freigegeben&#039;&#039;&#039;. Der Konsument darf denselben Schlüssel nach Korrektur des Inhalts erneut verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;5xx&#039;&#039;&#039; oder Exception || Der Schlüssel wird ebenfalls &#039;&#039;&#039;freigegeben&#039;&#039;&#039; und der Fall protokolliert. Der Konsument darf denselben Schlüssel erneut senden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Das 5xx-Verhalten wurde am 25.08.2026 gedreht.&#039;&#039;&#039; Vorher blieb der&lt;br /&gt;
Schlüssel nach einem Serverfehler belegt - begründet mit „niemals eine&lt;br /&gt;
Doppelbuchung&amp;quot;. Das war falsch gewichtet: der Vorgang wurde damit&lt;br /&gt;
&#039;&#039;&#039;unwiederholbar&#039;&#039;&#039;, der Client bekam dauerhaft 503 und hätte die erfasste Arbeit&lt;br /&gt;
verwerfen müssen. Garantierter Datenverlust wiegt schwerer als ein möglicher&lt;br /&gt;
Doppelsatz.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Folge für die Auslegung:&#039;&#039;&#039; Ein schreibender Endpunkt sollte selbst&lt;br /&gt;
wiederholbar sein, etwa über ein fachliches Merkmal des Clients. Wo reines&lt;br /&gt;
Anhängen stattfindet (Positionen, Dateien), bleibt sonst ein Restrisiko.}}&lt;br /&gt;
&lt;br /&gt;
Daraus folgen zwei Regeln für Endpunkt-Skripte:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Fachliche Ablehnungen als 4xx melden&#039;&#039;&#039; (422, 409, 404), nicht als 2xx mit Fehlertext im Body. Sonst wird die Ablehnung gespeichert und der Konsument bekommt sie bei jedem weiteren Versuch erneut - auch nachdem er den Fehler behoben hat.&lt;br /&gt;
* &#039;&#039;&#039;Die Antwort muss vollständig sein.&#039;&#039;&#039; Was zurückgegeben wird, wird eingefroren. Ein Feld, das erst der zweite Aufruf ergänzen würde, kommt beim Konsumenten nie an.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Ablauf und die Statuscodes stehen unter&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierungs-Skript==&lt;br /&gt;
&lt;br /&gt;
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 &#039;&#039;Authenticate&#039;&#039; bereitstellen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
    cPass: string;&lt;br /&gt;
begin&lt;br /&gt;
    // Eingangsdaten lesen&lt;br /&gt;
    cUser := &#039;&#039;;&lt;br /&gt;
    cPass := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;username&#039;, cUser);&lt;br /&gt;
    oReader.Str(&#039;password&#039;, cPass);&lt;br /&gt;
&lt;br /&gt;
    // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...&lt;br /&gt;
    if (PasswortPasst(cUser, cPass)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;           , 1);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_ID&#039;      , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039; , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_AUDIENCE&#039;, &#039;mandant1&#039;);&lt;br /&gt;
    end else begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Login fehlgeschlagen&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Im Anmeldeskript bleiben die _OBS_JWT_*-Felder Felder der&lt;br /&gt;
Antwort&#039;&#039;&#039; - anders als Status und Header, die zu Aufrufen des Writers geworden&lt;br /&gt;
sind. Der Grund: die Token-Ausstellung liest sie aus dem fertigen Körper, und&lt;br /&gt;
genau dort erwartet sie sie. Geschrieben werden sie deshalb wie jedes andere&lt;br /&gt;
Feld, nur eben mit dem Writer.}}&lt;br /&gt;
&lt;br /&gt;
Rückgabewerte:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! status !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| 1      || Erfolgreich, der Server erzeugt aus den &#039;&#039;_OBS_JWT_*&#039;&#039;-Werten einen Token (HS256)&lt;br /&gt;
|-&lt;br /&gt;
| 9      || Misserfolg, der Server antwortet mit &#039;&#039;&#039;401&#039;&#039;&#039; und reicht den Wert von &#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch (zusätzlich protokolliert)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Text aus &#039;&#039;error&#039;&#039; geht bei &#039;&#039;status&#039;&#039; = 9 &#039;&#039;&#039;an den Konsumenten&#039;&#039;&#039;&lt;br /&gt;
und wird typischerweise direkt unter dem Passwortfeld angezeigt. Er sollte für&lt;br /&gt;
Endanwender verständlich sein und keine internen Details verraten.}}&lt;br /&gt;
&lt;br /&gt;
Liefert das Skript einen anderen Status als 1 oder 9, ist gar nicht lauffähig oder&lt;br /&gt;
gibt kein auswertbares JSON zurück, antwortet der Server mit &#039;&#039;&#039;403&#039;&#039;&#039; und einer&lt;br /&gt;
generischen Meldung - ein defektes Anmeldeskript soll dem Anwender nicht als&lt;br /&gt;
„Passwort falsch&amp;quot; erscheinen.&lt;br /&gt;
&lt;br /&gt;
Kann der Server die &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; zum ausgestellten Token nicht schreiben&lt;br /&gt;
(Tabelle fehlt, Datenbankproblem), liefert er &#039;&#039;&#039;kein Token&#039;&#039;&#039; aus und antwortet&lt;br /&gt;
mit &#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Auch das ist bewusst kein 401/403: die&lt;br /&gt;
Zugangsdaten waren richtig, der Versuch darf wiederholt werden.&lt;br /&gt;
&lt;br /&gt;
===Aufbau der Token-Antwort===&lt;br /&gt;
&lt;br /&gt;
Bei Erfolg baut der Server die Antwort aus den Token-Feldern &#039;&#039;&#039;und allen weiteren&lt;br /&gt;
Feldern, die das Skript zurückgibt&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;:       &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;refreshToken&amp;quot;:&amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:   28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:  &amp;quot;2026-08-17T09:12:33+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Herkunft&lt;br /&gt;
|-&lt;br /&gt;
| token / accessToken || Derselbe Access-Token unter zwei Namen. &#039;&#039;accessToken&#039;&#039; erwarten die meisten Client-Bibliotheken, &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten&lt;br /&gt;
|-&lt;br /&gt;
| refreshToken || Nur wenn das Skript &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; geliefert hat&lt;br /&gt;
|-&lt;br /&gt;
| expiresIn || Laufzeit des Access-Tokens in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (&#039;&#039;JWT-Exp&#039;&#039; × 60)&lt;br /&gt;
|-&lt;br /&gt;
| serverTime || Serverzeit ISO-8601 mit Offset&lt;br /&gt;
|-&lt;br /&gt;
| alle übrigen Felder || &#039;&#039;&#039;Frei vom Skript bestimmt.&#039;&#039;&#039; Übernommen wird alles ausser &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Damit kann das Anmeldeskript alles mitliefern, was der Client direkt nach dem Login&lt;br /&gt;
braucht (Benutzer-Id, Mandant, Rollen, Berechtigungen) - ohne dass dieser dafür&lt;br /&gt;
einen zweiten Aufruf absetzen muss. Ein Feld, das genauso heisst wie eines der&lt;br /&gt;
Token-Felder oben, wird verworfen; die Engine setzt diese selbst.&lt;br /&gt;
&lt;br /&gt;
===Custom-Claims, serverTime und Refresh===&lt;br /&gt;
&lt;br /&gt;
Das Authenticate-Skript kann dem Token zusätzlich &#039;&#039;&#039;beliebige Custom-Claims&#039;&#039;&#039; mitgeben - z.B. Mandant und Rollen - und optional ein &#039;&#039;&#039;Refresh-Token&#039;&#039;&#039; anstoßen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! RÜckgabefeld !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt; || Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als &amp;lt;code&amp;gt;oReader.Claim(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_REFRESH_ID || (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_REFRESH_EXP || (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Jedes Token trägt automatisch den Claim &#039;&#039;token_use&#039;&#039; (&#039;&#039;access&#039;&#039; bzw. &#039;&#039;refresh&#039;&#039;) sowie die Sitzungskennungen &#039;&#039;sid&#039;&#039; und &#039;&#039;rid&#039;&#039; des Servers. Die Antwort enthält bei Erfolg &#039;&#039;serverTime&#039;&#039; (ISO-8601 mit Offset), bei ausgestelltem Refresh zusätzlich &#039;&#039;refreshToken&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;refreshToken&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;serverTime&amp;quot;:&amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Mandant und Rollen immer aus dem Token lesen (&#039;&#039;oReader.Claim(…)&#039;&#039;), nie aus Body oder Query.}}&lt;br /&gt;
&lt;br /&gt;
====Refresh-Methode====&lt;br /&gt;
&lt;br /&gt;
Der Refresh läuft über &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; und &#039;&#039;&#039;dasselbe Skript&#039;&#039;&#039;. Liegt am JWT-Endpunkt ein &#039;&#039;Authorization: Bearer &amp;lt;token&amp;gt;&#039;&#039; mit einem Refresh-Token vor, ruft der Server statt &#039;&#039;Authenticate&#039;&#039; die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf (ohne Bearer: Login; &#039;&#039;DELETE&#039;&#039; mit Access-Token: Abmelden, ohne Skript).&lt;br /&gt;
&lt;br /&gt;
Bevor das Skript läuft, hat der Server das vorgelegte Refresh-Token verifiziert&lt;br /&gt;
(Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;) und in der Sitzung &#039;&#039;&#039;entwertet&#039;&#039;&#039;.&lt;br /&gt;
Einmalgebrauch, Rotation und Sperrliste sind damit Sache des Servers. Das Skript&lt;br /&gt;
liefert nur noch die aktuellen Claims - genau das ist der Sinn des Aufrufs: eine&lt;br /&gt;
zwischenzeitlich geänderte Rolle oder ein gewechselter Mandant wirken spätestens&lt;br /&gt;
mit der nächsten Erneuerung.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal - einmal im Skript, einmal im&lt;br /&gt;
Server - und riskiert, dass beide Seiten unterschiedlicher Meinung sind. Der&lt;br /&gt;
Server sieht die Skript-Tabelle nicht, und das Skript sieht die Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Ein bereits benutztes Refresh-Token wird mit &#039;&#039;&#039;401&#039;&#039;&#039; abgelehnt. Erfolgt die&lt;br /&gt;
erneute Vorlage innerhalb von 60 Sekunden, bleibt die Sitzung bestehen (paralleler&lt;br /&gt;
Refresh einer App); danach gilt sie als Wiedervorlage und die &#039;&#039;&#039;gesamte Sitzung&lt;br /&gt;
wird gesperrt&#039;&#039;&#039;. Details in [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
Das alte Refresh-jti steht weiterhin als &#039;&#039;oReader.Param(&#039;_OBS_JWT_ID&#039;)&#039;&#039; bereit,&lt;br /&gt;
falls das Skript es für eigene Protokollierung braucht.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Refresh(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := oReader.Subject();&lt;br /&gt;
&lt;br /&gt;
    // Der Server hat das vorgelegte Refresh-Token bereits geprueft und&lt;br /&gt;
    // entwertet. Hier wird nur entschieden, ob der Benutzer die Sitzung&lt;br /&gt;
    // fortsetzen darf - und mit welchen Rechten.&lt;br /&gt;
    if (not BenutzerAktiv(cUser)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Konto ist nicht mehr aktiv, bitte neu anmelden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Rolle und Mandant neu lesen, nicht aus dem alten Token uebernehmen -&lt;br /&gt;
    // sonst wirkt ein Rechteentzug erst beim naechsten Login.&lt;br /&gt;
    oWriter.Int(&#039;status&#039;               , 1);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_ID&#039;          , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039;     , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_tenant&#039;, TechnikerMandant(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_roles&#039; , TechnikerRollen(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_REFRESH_ID&#039;  , GlobalUID());   // loest das neue Refresh-Token aus&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Für das Abmelden braucht das Skript keine Methode.&#039;&#039;&#039; Es läuft über ein&lt;br /&gt;
&#039;&#039;DELETE&#039;&#039; auf denselben JWT-Endpunkt (Access-Token im &#039;&#039;Authorization&#039;&#039;-Header)&lt;br /&gt;
und wird komplett in der Engine erledigt: die Sitzung wird gesperrt, die Antwort&lt;br /&gt;
ist 204. Eine Methode &#039;&#039;Logout&#039;&#039; gibt es nicht und wird nicht aufgerufen.&lt;br /&gt;
Details: [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Phase 4.&lt;br /&gt;
&lt;br /&gt;
Wer beim Abmelden zusätzlich fachlich etwas tun will - eine Geräteregistrierung&lt;br /&gt;
löschen, einen Push-Token verwerfen, den Vorgang protokollieren - nimmt einen&lt;br /&gt;
&#039;&#039;&#039;gewöhnlichen Endpunkt&#039;&#039;&#039; und ruft dort &#039;&#039;oWriter.JwtRevoke&#039;&#039;. Dasselbe gilt für&lt;br /&gt;
&amp;quot;auf allen Geräten abmelden&amp;quot;, weil das eine Berechtigungsentscheidung braucht:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    GeraeteRegistrierungLoeschen(oReader.Subject());&lt;br /&gt;
&lt;br /&gt;
    // &#039;session&#039; = diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;session&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Gesperrt wird in beiden Wegen die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039;, also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token vorheriger Erneuerungen. Ein wiederholter Aufruf ist&lt;br /&gt;
unschädlich. Lässt sich die Sitzung nicht sperren, überschreibt der Server die&lt;br /&gt;
Antwort des Skripts mit &#039;&#039;&#039;503&#039;&#039;&#039; - ein Client darf nicht glauben, er sei&lt;br /&gt;
abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
==Fehlerbehandlung im Skript==&lt;br /&gt;
&lt;br /&gt;
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 &#039;&#039;Interner Fehler&#039;&#039; und protokolliert die Detail-Meldung in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.&lt;br /&gt;
&lt;br /&gt;
Server-eigene Fehler (Routing, Rate-Limit, Token, Auffangnetz) haben ein&lt;br /&gt;
einheitliches Format mit einem &#039;&#039;error&#039;&#039;-Objekt - Aufbau siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].&lt;br /&gt;
&lt;br /&gt;
Für einen spezifischen Fehler an den Konsumenten gibt es &#039;&#039;&#039;einen&#039;&#039;&#039; Aufruf. Er&lt;br /&gt;
baut dieselbe Hülle, die auch die Server-eigenen Fehler tragen - Statuscode,&lt;br /&gt;
&#039;&#039;code&#039;&#039;, &#039;&#039;message&#039;&#039;, &#039;&#039;uid&#039;&#039; und &#039;&#039;traceId&#039;&#039; -, sodass beide Wege nicht&lt;br /&gt;
auseinanderlaufen können:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    if (Empty(oReader.Param(&#039;kundennr&#039;))) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Parameter &amp;quot;kundennr&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Error verwirft einen schon begonnenen Körper&#039;&#039;&#039; und setzt seinen&lt;br /&gt;
eigenen. Ein Skript darf also getrost Felder schreiben und danach noch&lt;br /&gt;
abbrechen - was der Client sieht, ist die Fehlerantwort. Nur ein &#039;&#039;&#039;offener&lt;br /&gt;
Stapel&#039;&#039;&#039; (ein &#039;&#039;ObjBegin&#039;&#039; ohne &#039;&#039;ObjEnd&#039;&#039;) bleibt auch dann ein Strukturfehler.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein fachlicher Fehler gehört auf einen &#039;&#039;&#039;4xx&#039;&#039;&#039;-Statuscode, nicht auf&lt;br /&gt;
200 mit Fehlertext im Body. Nur so erkennt der Konsument den Fehlschlag ohne den&lt;br /&gt;
Body auszuwerten - und nur so gibt der Server einen belegten&lt;br /&gt;
&#039;&#039;Idempotency-Key&#039;&#039; wieder frei (siehe Abschnitt Idempotenz).}}&lt;br /&gt;
&lt;br /&gt;
===Ändern: qSqlInit statt qSqlRead===&lt;br /&gt;
&lt;br /&gt;
Zum &#039;&#039;&#039;Ändern&#039;&#039;&#039; eines vorhandenen Satzes wird ebenfalls &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;&lt;br /&gt;
benutzt - der Satz wird über &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; adressiert:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;sys_uid&#039;  , cSysUid);      // Schreib-Index auf den Originalsatz&lt;br /&gt;
q.qSet(&#039;ti_status&#039;, &#039;9&#039;);&lt;br /&gt;
if (not q.SaveData(UPDATE_RECORD)) then begin&lt;br /&gt;
    // ... Fehler behandeln, siehe unten&lt;br /&gt;
end;&lt;br /&gt;
qSqlFree(q);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt; ist dafür die falsche Wahl, aus zwei Gründen:&lt;br /&gt;
&lt;br /&gt;
* Es liest den &#039;&#039;&#039;kompletten Altsatz&#039;&#039;&#039; ein - ein zusätzlicher Lesezugriff, der Zeit kostet.&lt;br /&gt;
* Es schreibt anschliessend nur die &#039;&#039;&#039;Änderungen&#039;&#039;&#039;. Ist der neue Wert gleich dem alten, entsteht gar kein Write, und &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt; liefert &#039;&#039;&#039;False&#039;&#039;&#039; - ununterscheidbar von einem echten Fehlschlag.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ohne &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; legt &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt; einen &#039;&#039;&#039;neuen&#039;&#039;&#039;&lt;br /&gt;
Datensatz an, statt den vorhandenen zu ändern. Die &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; ist der&lt;br /&gt;
Schreib-Index und gehört bei jeder Änderung gesetzt.}}&lt;br /&gt;
&lt;br /&gt;
Damit entfällt auch das übliche Paar aus &amp;lt;code&amp;gt;DB_LSeek&amp;lt;/code&amp;gt; und&lt;br /&gt;
&amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt;: &#039;&#039;&#039;eine&#039;&#039;&#039; Abfrage auf die &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; liefert&lt;br /&gt;
zugleich die Existenzprüfung und den Schreib-Index.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
cUid := &#039;&#039;;&lt;br /&gt;
if (DB_SOpen(oDB, &#039;SELECT sys_uid FROM tickets WHERE &#039; + cWhere + &#039; LIMIT 1&#039;, q)) then begin&lt;br /&gt;
    if (not q.EoF) then begin&lt;br /&gt;
        cUid := q.A2UID();&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
DB_Close(q);&lt;br /&gt;
&lt;br /&gt;
if (not Empty(cUid)) then begin&lt;br /&gt;
    // ändern: qSqlInit + sys_uid&lt;br /&gt;
end else begin&lt;br /&gt;
    // anlegen: qSqlInit ohne sys_uid&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Mehrere Felder sind EIN Schreibvorgang===&lt;br /&gt;
&lt;br /&gt;
Ein &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;-Objekt nimmt beliebig viele &amp;lt;code&amp;gt;qSet&amp;lt;/code&amp;gt; entgegen&lt;br /&gt;
und schreibt sie mit &#039;&#039;&#039;einem&#039;&#039;&#039; &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Fallen an derselben&lt;br /&gt;
Zeile mehrere Felder an, gehören sie in dasselbe Objekt:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
// FALSCH: fünf Felder, fünf Schreibvorgänge - und jeder Setzer, der sich&lt;br /&gt;
// seine sys_uid selbst holt, bringt zusätzlich eine eigene Abfrage mit&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_status&#039; , &#039;9&#039;);&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_grund&#039;  , cGrund);&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_von&#039;    , cBenutzer);&lt;br /&gt;
&lt;br /&gt;
// RICHTIG: ein Schreibobjekt, ein SaveData&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;sys_uid&#039;  , cUid);&lt;br /&gt;
q.qSet(&#039;ti_status&#039;, &#039;9&#039;);&lt;br /&gt;
q.qSet(&#039;ti_grund&#039; , cGrund);&lt;br /&gt;
q.qSet(&#039;ti_von&#039;   , cBenutzer);&lt;br /&gt;
if (not q.SaveData(UPDATE_RECORD)) then begin&lt;br /&gt;
    // ... Fehler behandeln&lt;br /&gt;
end;&lt;br /&gt;
qSqlFree(q);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Das ist nicht nur eine Frage der Geschwindigkeit: einzeln geschriebene Felder&lt;br /&gt;
sind &#039;&#039;&#039;nicht atomar&#039;&#039;&#039;. Bricht der dritte Schreibvorgang ab, steht die Zeile&lt;br /&gt;
halb geändert da - Grund gesetzt, Zeitpunkt nicht.&lt;br /&gt;
&lt;br /&gt;
Wird nichts gesetzt, darf auch nicht geschrieben werden: das Schreibobjekt dann&lt;br /&gt;
mit &amp;lt;code&amp;gt;qSqlFree&amp;lt;/code&amp;gt; freigeben, statt ein leeres &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;&lt;br /&gt;
abzusetzen.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Setzer für &#039;&#039;&#039;ein&#039;&#039;&#039; Feld (&amp;lt;code&amp;gt;FeldSetzen(cUid, cFeld, cWert)&amp;lt;/code&amp;gt;)&lt;br /&gt;
ist bequem und deshalb verführerisch. Er versteckt aber je Aufruf eine eigene&lt;br /&gt;
Abfrage und ein eigenes &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Für einen Vorgang mit mehreren&lt;br /&gt;
Feldern gehört ein &#039;&#039;&#039;Editier-Helfer&#039;&#039;&#039; her, der das Schreibobjekt liefert -&lt;br /&gt;
geschrieben wird einmal am Ende.}}&lt;br /&gt;
&lt;br /&gt;
===Keine Abfrage je Zeile===&lt;br /&gt;
&lt;br /&gt;
Was in einer Liste je Zeile nachgeschlagen wird, kostet &#039;&#039;&#039;N&#039;&#039;&#039; Abfragen für&lt;br /&gt;
&#039;&#039;&#039;eine&#039;&#039;&#039; Antwort. Bei fünfzig Aufträgen sind das fünfzig Abfragen für ein&lt;br /&gt;
einziges Feld. Der Ausweg ist immer derselbe:&lt;br /&gt;
&lt;br /&gt;
* Werte aus einer 1:1-Beziehung über einen &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; mitnehmen - bei einem &#039;&#039;&#039;eindeutigen&#039;&#039;&#039; Index auf dem Join-Feld kann er die Zeilen nicht vervielfachen.&lt;br /&gt;
* Einzelwerte aus einer 1:n-Beziehung über eine &#039;&#039;&#039;Unterabfrage&#039;&#039;&#039; in der SELECT-Liste holen, nicht über einen Join (der Join würde die Zeilen vervielfachen).&lt;br /&gt;
* Eine echte Unterliste (Positionen, Dateien) nur dann nachladen, wenn ein &#039;&#039;&#039;Zählfeld&#039;&#039;&#039; in der Hauptabfrage sagt, dass es überhaupt eine gibt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein GET soll &#039;&#039;&#039;lesen&#039;&#039;&#039;. Schreibvorgänge im Lesepfad - etwas&lt;br /&gt;
&amp;quot;beim ersten Ausliefern festschreiben&amp;quot; - vervielfachen sich mit der Zeilenzahl&lt;br /&gt;
und treffen den Aufrufer, der nur eine Liste angefordert hat.}}&lt;br /&gt;
&lt;br /&gt;
===Schreibfehler erkennen===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;&amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt; liefert einen Boolean - und der ist die einzige&lt;br /&gt;
Fehlermeldung, die es gibt.&#039;&#039;&#039; (Auch der &#039;&#039;&#039;Parameter&#039;&#039;&#039; ist ein Boolean:&lt;br /&gt;
&#039;&#039;NEW_RECORD&#039;&#039; und &#039;&#039;UPDATE_RECORD&#039;&#039; sind Wahrheitswerte, keine Zahlen.) Schlägt das Schreiben in der Datenbank fehl&lt;br /&gt;
(&#039;&#039;Duplicate entry&#039;&#039; auf einem eindeutigen Index, zu langer Wert, fehlende&lt;br /&gt;
Spalte), dann&lt;br /&gt;
&lt;br /&gt;
* wird &#039;&#039;&#039;keine&#039;&#039;&#039; Exception ausgelöst,&lt;br /&gt;
* steht &#039;&#039;&#039;nichts&#039;&#039;&#039; in &#039;&#039;RESTSRV_PROTO&#039;&#039;,&lt;br /&gt;
* läuft das Skript weiter und antwortet mit dem Erfolg, den es selbst formuliert.&lt;br /&gt;
&lt;br /&gt;
Wer den Aufruf als Anweisung schreibt, baut damit einen stillen Datenverlust ein.&lt;br /&gt;
Weil in einem Endpunkt schnell ein Dutzend Schreibvorgänge zusammenkommen, gehört&lt;br /&gt;
die Prüfung in eine eigene kleine Prozedur - am besten in die gemeinsame Lib:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Speichern(q: TqSQL; lNeu: Boolean; const cWas: string);&lt;br /&gt;
var lOk: Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    lOk := q.SaveData(lNeu);&lt;br /&gt;
    qSqlFree(q);                     // auch im Fehlerfall freigeben&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        raise Exception.Create(&#039;Schreiben fehlgeschlagen: &#039; + cWas);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
// Aufruf statt SaveData + qSqlFree:&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;ti_betreff&#039;, cBetreff);&lt;br /&gt;
Speichern(q, NEW_RECORD, &#039;Ticket&#039;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Exception fängt der Server: er schreibt &#039;&#039;Schreiben fehlgeschlagen: Ticket&#039;&#039;&lt;br /&gt;
nach &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; und antwortet mit &#039;&#039;&#039;500&#039;&#039;&#039;. Liegt ein&lt;br /&gt;
&#039;&#039;Idempotency-Key&#039;&#039; an, wird er dabei &#039;&#039;&#039;freigegeben&#039;&#039;&#039; - die Anfrage ist also&lt;br /&gt;
wiederholbar (siehe Abschnitt Idempotenz).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein fehlgeschlagenes Schreiben ist kein Fall, in dem ein Endpunkt&lt;br /&gt;
sinnvoll weiterlaufen kann: die Fachwirkung ist dann unvollständig. Wer trotzdem&lt;br /&gt;
weitermachen will - etwa weil mehrere unabhängige Sätze geschrieben werden -,&lt;br /&gt;
prüft den Rückgabewert und baut selbst eine Antwort. Ignorieren darf man ihn&lt;br /&gt;
nie.}}&lt;br /&gt;
&lt;br /&gt;
Gängige Codes, die auch der Server selbst verwendet: &#039;&#039;VALIDATION_FAILED&#039;&#039; (422),&lt;br /&gt;
&#039;&#039;NOT_FOUND&#039;&#039; (404), &#039;&#039;FORBIDDEN_ROLE&#039;&#039; (403), &#039;&#039;VERSION_CONFLICT&#039;&#039; (409).&lt;br /&gt;
Eigene, fachlich sprechende Codes sind erlaubt - wichtig ist, dass sie stabil&lt;br /&gt;
bleiben, weil Konsumenten darauf ihre Reaktion aufbauen.&lt;br /&gt;
&lt;br /&gt;
==Gemeinsamen Code auslagern==&lt;br /&gt;
&lt;br /&gt;
Wird Logik in mehreren Endpunkten gebraucht - Berechtigungsprüfungen,&lt;br /&gt;
JSON-Bausteine, Hilfsfunktionen -, muss sie nicht in jedes Skript kopiert werden.&lt;br /&gt;
Die Skript-Engine lädt Textbausteine beim Laden aus einer beliebigen Tabelle nach:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{$L T=&amp;quot;restsrv_endpoints&amp;quot; I=&amp;quot;re_pathtemplate&amp;quot; V=&amp;quot;/meinapp/lib&amp;quot; F=&amp;quot;re_script&amp;quot;}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Attribut !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;T&#039;&#039;&#039; || Tabelle, aus der geladen wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;I&#039;&#039;&#039; || Spalte, über die gesucht wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;V&#039;&#039;&#039; || Wert, der in dieser Spalte stehen muss&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;F&#039;&#039;&#039; || Spalte, deren Inhalt an dieser Stelle eingesetzt wird&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Direktive steht üblicherweise direkt unter dem Kopfkommentar des Skripts. Der&lt;br /&gt;
Compiler sieht danach den zusammengesetzten Text - eine Funktion aus der Lib lässt&lt;br /&gt;
sich also aufrufen wie eine im Skript selbst geschriebene.&lt;br /&gt;
&lt;br /&gt;
===Bewährtes Muster: Lib als eigene Endpunkt-Zeile===&lt;br /&gt;
&lt;br /&gt;
Am wenigsten Verwaltung macht es, die Lib &#039;&#039;&#039;als eigene Zeile in&lt;br /&gt;
RESTSRV_ENDPOINTS&#039;&#039;&#039; abzulegen:&lt;br /&gt;
&lt;br /&gt;
# Endpunkt anlegen mit einem eigenen Pfad-Template, z.B. &amp;lt;code&amp;gt;/meinapp/lib&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Den Lib-Quelltext ins Skript-Feld dieser Zeile schreiben (F7).&lt;br /&gt;
# &#039;&#039;&#039;Keine Berechtigung&#039;&#039;&#039; in der Berechtigungs-Liste vergeben - das ist der Schutz: ohne Berechtigung beantwortet der Server einen Aufruf mit 403.&lt;br /&gt;
# In jedem nutzenden Endpunkt die Direktive oben einfügen.&lt;br /&gt;
&lt;br /&gt;
Damit ist die Lib reiner Ablageort und von aussen nicht nutzbar. Eine Änderung&lt;br /&gt;
wirkt auf alle einbindenden Endpunkte, ohne dass ein einziges Endpunkt-Skript&lt;br /&gt;
angefasst werden muss.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Lib und Skripte gemeinsam einspielen.&#039;&#039;&#039; Kommt in der Lib eine neue&lt;br /&gt;
Funktion hinzu, genügt es nicht, nur das nutzende Skript zu aktualisieren. Eine&lt;br /&gt;
ältere Lib lädt fehlerfrei - der Compiler meldet dann ausschliesslich die neuen&lt;br /&gt;
Namen als &#039;&#039;Unknown name&#039;&#039;, was leicht wie ein Fehler im Skript aussieht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Auch hier greift der Endpunkt-Cache: eine Änderung an der Lib wird erst&lt;br /&gt;
nach Ablauf des TTL (bis zu 60 Sekunden) wirksam.}}&lt;br /&gt;
&lt;br /&gt;
===Wohin gehört welcher Code?===&lt;br /&gt;
&lt;br /&gt;
Drei Ebenen, und die Zuordnung entscheidet, wie schnell eine Änderung wirkt:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Art des Codes !! Wohin !! Wirksam&lt;br /&gt;
|-&lt;br /&gt;
| Fachlogik **einer** Anwendung - Feldnamen, Zustandswerte, Rollenregeln, Schreibziele || in die &#039;&#039;&#039;DWS-Lib dieser Anwendung&#039;&#039;&#039; (eigene Endpunkt-Zeile, per &amp;lt;code&amp;gt;{$L …}&amp;lt;/code&amp;gt; eingebunden) || nach Ablauf des Cache-TTL, ohne Build&lt;br /&gt;
|-&lt;br /&gt;
| Mechanik, die &#039;&#039;&#039;mehrere&#039;&#039;&#039; Anwendungen brauchen - und auch das OBS-Umfeld || nach &#039;&#039;&#039;obs_lib&#039;&#039;&#039; als Delphi-Unit || erst nach Build und Rollout&lt;br /&gt;
|-&lt;br /&gt;
| Mechanik, die &#039;&#039;&#039;nur&#039;&#039;&#039; der REST-Server braucht || in die Units des Servers || erst nach Build und Rollout&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Regel hat einen praktischen Grund und keinen ästhetischen: Fachlogik ändert&lt;br /&gt;
sich in Tagen, Mechanik in Monaten. Was in der Lib liegt, ist nach einer Minute&lt;br /&gt;
aktiv; was in einer Unit liegt, braucht einen Build. Wer Fachlogik nach unten&lt;br /&gt;
schiebt, tauscht Geschwindigkeit gegen nichts ein.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Anwendung, die ihre eigene Lib hat, kopiert sie &#039;&#039;&#039;nicht&#039;&#039;&#039; von&lt;br /&gt;
einer anderen. Zwei Kopien derselben Lib driften auseinander, und die Abweichung&lt;br /&gt;
fällt erst auf, wenn eine der beiden Anwendungen etwas Falsches tut. Geteiltes&lt;br /&gt;
gehört eine Ebene tiefer.}}&lt;br /&gt;
&lt;br /&gt;
==Fallstricke im Skript==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Punkte unterscheiden das Skript von normalem Delphi-Code und kosten&lt;br /&gt;
sonst eine Runde Fehlersuche:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Stolperstein !! Richtig&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DB_SOpen(&#039;Y00…&#039;, oDB, …)&#039;&#039; - der AD-UID-Parameter aus dem Delphi-Code || Im Skript &#039;&#039;&#039;ohne UID&#039;&#039;&#039;: &amp;lt;code&amp;gt;DB_SOpen(oDB, cSql, q)&amp;lt;/code&amp;gt;. Gleiches gilt für &amp;lt;code&amp;gt;qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;qSqlRead(oDB, &#039;tab&#039;, cWhere)&amp;lt;/code&amp;gt; - jede dieser Funktionen beginnt mit &#039;&#039;oDB&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;oReader.Str&amp;amp;lt;T&amp;amp;gt;(…)&#039;&#039; oder irgendein Generic || Generics gibt es in der Skriptsprache nicht. Die Leser sind je Typ eigene Methoden: &#039;&#039;Str&#039;&#039;, &#039;&#039;Int&#039;&#039;, &#039;&#039;Num&#039;&#039;, &#039;&#039;Bool&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Eine eigene Prozedur &#039;&#039;Delete&#039;&#039; für DELETE || &#039;&#039;Delete&#039;&#039; ist eine &#039;&#039;&#039;Standardprozedur&#039;&#039;&#039; der Sprache. Die Deklaration lässt sich nicht übersetzen, und die Meldung nennt nur die Zeile - nicht die Ursache&lt;br /&gt;
|-&lt;br /&gt;
| Ein Helfer, der ein JSON-Fragment als String zurückgibt || Den &#039;&#039;&#039;Writer als Parameter&#039;&#039;&#039; übergeben und an der richtigen Stelle hineinschreiben. Fragmente von Hand zusammenzusetzen ist genau der Weg, auf dem die Reihenfolge und die Klammern verloren gingen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;oWriter.ArrEnd&#039;&#039; nach &#039;&#039;RootArr&#039;&#039; || &#039;&#039;RootArr&#039;&#039; öffnet &#039;&#039;&#039;keine&#039;&#039;&#039; Ebene, es bestimmt nur die Klammer der Wurzel. Ein &#039;&#039;ArrEnd&#039;&#039; dazu ist ein Strukturfehler&lt;br /&gt;
|-&lt;br /&gt;
| Zahlen selbst formatieren (&#039;&#039;NStr&#039;&#039;, &#039;&#039;StrTran&#039;&#039;) || &amp;lt;code&amp;gt;oWriter.Num(&#039;preis&#039;, nWert, 2)&amp;lt;/code&amp;gt;. Selbst formatiert stand bei Werten ab 1000 der Tausendertrenner im JSON - gültiges JSON, falscher Wert, keine Meldung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;EMPTY_DATE&#039;&#039; für ein leeres Datum || &#039;&#039;EMPTY_DATE&#039;&#039; ist im Skript ein &#039;&#039;&#039;String&#039;&#039;&#039;. Für Datumsvariablen und -vergleiche &#039;&#039;&#039;MINDATETIME&#039;&#039;&#039; verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;fVal&#039;&#039; auf einen JSON-Zahlenwert || JSON liefert den Dezimalpunkt, &#039;&#039;fVal&#039;&#039; erwartet die lokale Notation: vorher &amp;lt;code&amp;gt;StrTran(cVal, &#039;.&#039;, &#039;,&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;TStringList&#039;&#039; für Zwischenlisten || In Skripten unzuverlässig. Kleine Mengen über einen Delimiter-String führen (&amp;lt;code&amp;gt;&#039;&amp;amp;#124;&#039; + wert + &#039;&amp;amp;#124;&#039;&amp;lt;/code&amp;gt;) und mit &#039;&#039;Pos&#039;&#039; prüfen&lt;br /&gt;
|-&lt;br /&gt;
| Default-Parameter in eigenen Funktionen || Werden nicht unterstützt - alle Parameter ausschreiben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;q.SaveData(…)&amp;lt;/code&amp;gt; als Anweisung schreiben || &#039;&#039;SaveData&#039;&#039; liefert einen &#039;&#039;&#039;Boolean&#039;&#039;&#039;. Ein SQL-Fehler beim Schreiben wirft &#039;&#039;&#039;keine&#039;&#039;&#039; Exception und landet in &#039;&#039;&#039;keinem&#039;&#039;&#039; Protokoll - der Rückgabewert ist die einzige Meldung. Immer auswerten, siehe [[#Schreibfehler erkennen|Schreibfehler erkennen]]&lt;br /&gt;
|-&lt;br /&gt;
| Je Feld ein eigener Setzer-Aufruf || Ein &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;-Objekt, beliebig viele &amp;lt;code&amp;gt;qSet&amp;lt;/code&amp;gt;, &#039;&#039;&#039;ein&#039;&#039;&#039; &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Einzeln geschrieben kostet jedes Feld eine eigene Abfrage samt eigenem Write - und der Vorgang ist nicht mehr atomar, siehe [[#Mehrere Felder sind EIN Schreibvorgang|Mehrere Felder]]&lt;br /&gt;
|-&lt;br /&gt;
| Ein Wert je Zeile nachgeschlagen || In einer Liste sind das N Abfragen für eine Antwort. Über &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; (1:1) oder Unterabfrage (1:n) mitnehmen, siehe [[#Keine Abfrage je Zeile|Keine Abfrage je Zeile]]&lt;br /&gt;
|-&lt;br /&gt;
| Ausgabeparameter vom Typ &amp;lt;code&amp;gt;var … : TDateTime&amp;lt;/code&amp;gt; || Bringt die Skript-VM zum &#039;&#039;&#039;Absturz&#039;&#039;&#039; (Assertion in &#039;&#039;dwsStack.pas&#039;&#039;), und zwar beim ersten &#039;&#039;&#039;Lesen&#039;&#039;&#039; des Werts - nicht schon beim Schreiben, die Ursache liegt also nicht dort, wo der Fehler auffällt. &amp;lt;code&amp;gt;var string&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;var Integer&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;var Double&amp;lt;/code&amp;gt; laufen. Zeitpunkte als ISO-Zeichenkette zurückgeben und erst beim Aufrufer wandeln&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Skript-Cache==&lt;br /&gt;
&lt;br /&gt;
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: &#039;&#039;sys_date&#039;&#039; 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.&lt;br /&gt;
&lt;br /&gt;
==Verfügbare Bibliotheken==&lt;br /&gt;
&lt;br /&gt;
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Base.Tools&#039;&#039; - String-, Datum-, IIF-, Empty-Helper&lt;br /&gt;
* &#039;&#039;Base.DB&#039;&#039; / &#039;&#039;Base.xQuery&#039;&#039; - Datenbank-Operationen&lt;br /&gt;
* &#039;&#039;Base.qSqlReg&#039;&#039; - Schreibzugriffe&lt;br /&gt;
* &#039;&#039;lib_ScriptIO&#039;&#039; - Writer und Reader, siehe unten. &#039;&#039;&#039;System.JSON braucht ein Endpunkt-Skript nicht&#039;&#039;&#039; - der Writer erzeugt die Antwort, der Reader liest den Körper&lt;br /&gt;
* &#039;&#039;Base.ToolsConst&#039;&#039; - Konstanten wie &#039;&#039;CRLF&#039;&#039;, &#039;&#039;SINGELQUOTE&#039;&#039;, &#039;&#039;MINDATETIME&#039;&#039;&lt;br /&gt;
* &#039;&#039;lib_ScriptIO&#039;&#039; - &#039;&#039;TxRestWriter&#039;&#039;, &#039;&#039;TxRestReader&#039;&#039; und die Zeitfunktionen &#039;&#039;RestIsoFromDate&#039;&#039; / &#039;&#039;RestDateFromIso&#039;&#039; / &#039;&#039;RestOffsetToMinutes&#039;&#039;. Writer und Reader bekommt das Skript als Parameter; die Zeitfunktionen ruft es direkt&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Aufrufe mit ihren Skript-Signaturen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Zweck !! Aufruf&lt;br /&gt;
|-&lt;br /&gt;
| Lesen || &amp;lt;code&amp;gt;DB_SOpen(oDB, cSql, q)&amp;lt;/code&amp;gt;, dann &amp;lt;code&amp;gt;q.EoF&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.Next()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.A2C(&#039;feld&#039;)&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2I&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2F&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2D&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2UID()&amp;lt;/code&amp;gt;, am Ende &amp;lt;code&amp;gt;DB_Close(q)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Neu anlegen || &amp;lt;code&amp;gt;q := qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(&#039;feld&#039;, wert)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;if (not q.SaveData(NEW_RECORD)) then …&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;qSqlFree(q)&amp;lt;/code&amp;gt; – der Rückgabewert ist Pflicht, siehe [[#Schreibfehler erkennen|Schreibfehler erkennen]]&lt;br /&gt;
|-&lt;br /&gt;
| Ändern || &amp;lt;code&amp;gt;q := qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(&#039;sys_uid&#039;, cUid)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(…)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;if (not q.SaveData(UPDATE_RECORD)) then …&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;qSqlFree(q)&amp;lt;/code&amp;gt; – &#039;&#039;&#039;nicht&#039;&#039;&#039; &amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt;, siehe [[#Ändern: qSqlInit statt qSqlRead|Ändern]]. Alle Felder derselben Zeile in &#039;&#039;&#039;ein&#039;&#039;&#039; Objekt, siehe [[#Mehrere Felder sind EIN Schreibvorgang|Mehrere Felder]]&lt;br /&gt;
|-&lt;br /&gt;
| Direkt ausführen || &amp;lt;code&amp;gt;DB_SqlExec(oDB, cSql)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Existenzprüfung || &amp;lt;code&amp;gt;DB_LSeek(oDB, &#039;tab&#039;, cWhere)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Werte quoten (Pflicht) || &amp;lt;code&amp;gt;DB_SQLVal(wert)&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Alle diese Funktionen beginnen im Skript mit &#039;&#039;oDB&#039;&#039;. Ein zusätzlicher&lt;br /&gt;
AD-UID-Parameter als erstes Argument gehört zur Delphi-Variante und lässt sich im&lt;br /&gt;
Skript nicht übersetzen.}}&lt;br /&gt;
&lt;br /&gt;
Konkrete Beispiele:&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Endpunkte&amp;diff=65542</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Endpunkte</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Endpunkte&amp;diff=65542"/>
		<updated>2026-09-25T09:28:32Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Idempotency-Key als Pflicht, Aufraeumen offener Reservierungen berichtigt, Fehlercodes 400/503/308 ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
= Endpunkte =&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; stellt eine Adresse bereit, über die der REST-Server konkrete&lt;br /&gt;
Funktionen anbietet. Jeder Endpunkt wird durch ein OBS-Skript oder einen WebHook&lt;br /&gt;
realisiert und ist genau einem Server-Profil zugeordnet.&lt;br /&gt;
&lt;br /&gt;
== Routing über Pfad-Templates ==&lt;br /&gt;
&lt;br /&gt;
Das Routing erfolgt über die Spalte &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_pathtemplate&amp;lt;/code&amp;gt;).&lt;br /&gt;
Ein Template beschreibt den vollständigen Pfad nach dem Host und kann&lt;br /&gt;
&#039;&#039;&#039;Platzhalter&#039;&#039;&#039; enthalten:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Statische Segmente&#039;&#039;&#039; müssen exakt übereinstimmen (Groß-/Kleinschreibung wird ignoriert).&lt;br /&gt;
* &#039;&#039;&#039;Platzhalter&#039;&#039;&#039; in geschweiften Klammern – z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;{uid}&amp;lt;/code&amp;gt; – passen auf einen beliebigen Wert und werden als [[#Pfad-Parameter im Skript|Pfad-Parameter]] erfasst.&lt;br /&gt;
&lt;br /&gt;
Beispiele für gültige Templates:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Template !! Passt auf !! Pfad-Parameter&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || –&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/4711&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;uid = 4711&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/4711/modules/A1&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;uid = 4711&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code = A1&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/kalender/v1&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/kalender/v1&amp;lt;/code&amp;gt; || –&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Präzedenz bei mehreren Treffern ===&lt;br /&gt;
&lt;br /&gt;
Passen mehrere Templates auf denselben Pfad, &#039;&#039;&#039;gewinnt das spezifischste&#039;&#039;&#039; –&lt;br /&gt;
also das mit den meisten statischen Segmenten. So schlägt &amp;lt;code&amp;gt;/orders/summary&amp;lt;/code&amp;gt;&lt;br /&gt;
das Template &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;, während &amp;lt;code&amp;gt;/orders/4711&amp;lt;/code&amp;gt; auf&lt;br /&gt;
&amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; matcht.&lt;br /&gt;
&lt;br /&gt;
== Hauptfelder eines Endpunkts ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Spalte !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_pathtemplate&amp;lt;/code&amp;gt; || &#039;&#039;&#039;Maßgeblich fürs Routing.&#039;&#039;&#039; Vollständiger Pfad mit Platzhaltern, z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Server&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_server&amp;lt;/code&amp;gt; || Zuordnung zum Server-Profil (erforderlich)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Aktiv&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_aktiv&amp;lt;/code&amp;gt; || Statusflag; inaktive Endpunkte liefern 404&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skript&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_script&amp;lt;/code&amp;gt; || DwScript-Quelltext des Handlers (siehe [[/OBS/Kostenpflichtige_Module/RESTServer/Scripting|Scripting]])&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;WebHook&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_webhook&amp;lt;/code&amp;gt; || optional: Verweis auf einen Einmal-Endpunkt statt Skript&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Info&#039;&#039;&#039; || – || Dokumentations-Freitext&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt; || Erlaubt Datei-Uploads an diesem Endpunkt (&amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; = aus, &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt; = an). Ohne Freigabe werden Upload-Anfragen mit 415 abgelehnt.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; || Maximale Dateigrösse in MB. Leer/0 = Standard 25&amp;amp;nbsp;MB. Überschreitung wird mit 413 abgelehnt.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Pfad-Parameter im Skript ==&lt;br /&gt;
&lt;br /&gt;
Die aus den Platzhaltern erfassten Werte liest das Endpunkt-Skript über&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Path&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUid := oReader.Path(&#039;uid&#039;);   // aus /orders/{uid}&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Werte stammen aus dem Routing und können nicht durch den Client&lt;br /&gt;
überschrieben werden.&lt;br /&gt;
&lt;br /&gt;
== Datei-Uploads ==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt nimmt Datei-Uploads nur entgegen, wenn er dafür freigeschaltet ist&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;). Andernfalls werden Upload-Anfragen mit&lt;br /&gt;
&#039;&#039;&#039;415 Unsupported Media Type&#039;&#039;&#039; abgelehnt und das Skript wird nicht ausgeführt.&lt;br /&gt;
&lt;br /&gt;
Die maximal zulässige Dateigrösse wird pro Endpunkt über &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;&lt;br /&gt;
(in MB) festgelegt; ohne Angabe gilt der Standard von &#039;&#039;&#039;25&amp;amp;nbsp;MB&#039;&#039;&#039;. Wird das&lt;br /&gt;
Limit überschritten, antwortet der Server mit &#039;&#039;&#039;413 Payload Too Large&#039;&#039;&#039;. Für&lt;br /&gt;
Uploads gilt nicht das JSON-Body-Limit (10&amp;amp;nbsp;MB), sondern dieses Endpunkt-Limit.&lt;br /&gt;
Der Originalname (beim resumable Upload über den Header &amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt;) ist &#039;&#039;&#039;Pflicht&#039;&#039;&#039;; fehlt er, wird der Upload mit &#039;&#039;&#039;400&#039;&#039;&#039; abgelehnt, ohne dass eine Datei angelegt wird. Das Skript liest Pfad, Name, Content-Type und die SHA-256-Prüfsumme der gespeicherten Datei über &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadName()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadType()&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;UploadSha()&amp;lt;/code&amp;gt;; bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; die übrigen Formularfelder über &amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Das Übertragungsprotokoll&#039;&#039;&#039; - Header, Statuscodes, Resume-Verhalten - steht auf einer eigenen Seite: [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]].&lt;br /&gt;
&lt;br /&gt;
Unterstützt werden zwei Übertragungsarten:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfacher Upload&#039;&#039;&#039; per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; (eine Datei pro Request).&lt;br /&gt;
* &#039;&#039;&#039;Resumable/Chunked Upload&#039;&#039;&#039; per &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; (grosse Dateien, fortsetzbar nach Abbruch).&lt;br /&gt;
&lt;br /&gt;
Der Server legt die hochgeladene Datei in einem temporären Verzeichnis ab und&lt;br /&gt;
übergibt dem Endpunkt-Skript den Pfad. Was mit der Datei geschieht (Ablage,&lt;br /&gt;
DMS-Verknüpfung, Weiterverarbeitung), entscheidet allein das Skript. Details und&lt;br /&gt;
Beispiele: [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
== Datei-Downloads ==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt kann eine &#039;&#039;&#039;Datei&#039;&#039;&#039; statt eines JSON-Körpers zurückgeben -&lt;br /&gt;
PDF, CSV, eine APK. Dafür ist &#039;&#039;&#039;keine Freischaltung&#039;&#039;&#039; nötig: ein Download&lt;br /&gt;
entsteht dadurch, dass das Skript ihn mit &amp;lt;code&amp;gt;oWriter.SendFile(...)&amp;lt;/code&amp;gt;&lt;br /&gt;
bzw. &amp;lt;code&amp;gt;oWriter.SendTempFile(...)&amp;lt;/code&amp;gt; erzeugt, nicht dadurch, dass ein&lt;br /&gt;
Client ihn anfragt.&lt;br /&gt;
&lt;br /&gt;
Content-Type, Dateiname, Bereichsanfragen (&amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;, &#039;&#039;&#039;206&#039;&#039;&#039;),&lt;br /&gt;
ETag und das Aufräumen temporärer Dateien übernimmt der Server. Einzelheiten:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].&lt;br /&gt;
&lt;br /&gt;
== HTTP-Methode ==&lt;br /&gt;
&lt;br /&gt;
Welche Funktion aufgerufen wird, ergibt sich aus dem HTTP-Verb: Der Server ruft&lt;br /&gt;
die gleichnamige Skript-Methode auf (&amp;lt;code&amp;gt;Get&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Post&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Put&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Patch&amp;lt;/code&amp;gt;).&lt;br /&gt;
Ein Template entspricht damit &#039;&#039;&#039;einem&#039;&#039;&#039; Endpunkt-Skript; unterschiedliche&lt;br /&gt;
Pfad-Formen (z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; vs. &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;) sind&lt;br /&gt;
&#039;&#039;&#039;eigene Endpunkt-Einträge&#039;&#039;&#039; mit eigenem Skript und eigener Berechtigung.&lt;br /&gt;
&lt;br /&gt;
== URL-Struktur ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
http://[Host][:Port][Pfad-Template]&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders/4711&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders/4711/modules/A1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Versionierung ==&lt;br /&gt;
&lt;br /&gt;
Da es kein eigenes Versions-Feld mehr gibt, wird die Version als statisches&lt;br /&gt;
Segment ins Template aufgenommen, z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/v1&amp;lt;/code&amp;gt; oder&lt;br /&gt;
&amp;lt;code&amp;gt;/v1/orders/{uid}&amp;lt;/code&amp;gt;. Änderung ohne Breaking Change:&lt;br /&gt;
&lt;br /&gt;
# Bestehenden Endpunkt unverändert lassen.&lt;br /&gt;
# Neuen Endpunkt mit gleichem Ressourcennamen, aber neuem Versions-Segment im Template anlegen.&lt;br /&gt;
# Berechtigungen für neue Konsumenten setzen.&lt;br /&gt;
# Alten Endpunkt deaktivieren, wenn die Migration abgeschlossen ist.&lt;br /&gt;
&lt;br /&gt;
== Zugriffskontrolle ==&lt;br /&gt;
&lt;br /&gt;
Endpunkte sind Server-Profilen zugeordnet; ein Zugang benötigt eine explizite&lt;br /&gt;
Berechtigung (Tabelle &amp;lt;code&amp;gt;RESTSRV_ACCESS&amp;lt;/code&amp;gt;), um einen Endpunkt nutzen zu&lt;br /&gt;
dürfen.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Ein Endpunkt besteht aus zwei Zeilen, und beide werden von Hand&lt;br /&gt;
gepflegt:&#039;&#039;&#039; der Eintrag in &amp;lt;code&amp;gt;RESTSRV_ENDPOINTS&amp;lt;/code&amp;gt; und die Berechtigung&lt;br /&gt;
in &amp;lt;code&amp;gt;RESTSRV_ACCESS&amp;lt;/code&amp;gt;. Fehlt die zweite, antwortet der Endpunkt&lt;br /&gt;
&#039;&#039;&#039;403&#039;&#039;&#039; - und sieht dabei fertig aus: Pfad, Skript und Server-Profil stehen&lt;br /&gt;
korrekt da, der Aufruf kommt trotzdem nicht durch. Das ist die häufigste Ursache&lt;br /&gt;
für ein „der Endpunkt ist doch angelegt&amp;quot; und kostet jedes Mal eine&lt;br /&gt;
Fehlersuche, weil der Statuscode nach einem Rechteproblem des Konsumenten&lt;br /&gt;
aussieht und nicht nach einer fehlenden Konfigurationszeile.&lt;br /&gt;
&lt;br /&gt;
Beim Anlegen also &#039;&#039;&#039;immer beide Zeilen&#039;&#039;&#039;, und beim Prüfen eines&lt;br /&gt;
&amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; zuerst nachsehen, ob die zweite existiert.}} Weil jede Pfad-Form ein eigener Endpunkt-Eintrag ist, lässt sich der&lt;br /&gt;
Zugriff &#039;&#039;&#039;pro Pfad-Form&#039;&#039;&#039; granular vergeben (z.&amp;amp;nbsp;B. Lesen von&lt;br /&gt;
&amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; erlauben, aber das ändernde&lt;br /&gt;
&amp;lt;code&amp;gt;PUT /orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; nicht).&lt;br /&gt;
&lt;br /&gt;
== Caching ==&lt;br /&gt;
&lt;br /&gt;
Die Endpunkt-Definitionen werden pro Server-Profil zwischengespeichert&lt;br /&gt;
(TTL, Standard 60&amp;amp;nbsp;s). Neue oder geänderte Endpunkte/Templates werden daher&lt;br /&gt;
erst nach Ablauf des Caches (bzw. nach einer Cache-Invalidierung) wirksam – nicht&lt;br /&gt;
zwingend sofort.&lt;br /&gt;
&lt;br /&gt;
== HTTP-Fehlercodes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Code !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;404&#039;&#039;&#039; || Kein Template passt zum Pfad, Endpunkt inaktiv oder falsches Server-Profil&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;403&#039;&#039;&#039; || Zugang fehlt in der Berechtigungsliste&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;401&#039;&#039;&#039; || API-Key ungültig/fehlend, JWT abgelaufen oder Sitzung gesperrt (&#039;&#039;AUTH_EXPIRED&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;500&#039;&#039;&#039; || Skript- oder Syntax-Fehler (Details in &amp;lt;code&amp;gt;RESTSRV_PROTO&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;405&#039;&#039;&#039; || Das Endpunkt-Skript hat für die angefragte HTTP-Methode keine Funktion; der Header &amp;lt;code&amp;gt;Allow&amp;lt;/code&amp;gt; nennt die vorhandenen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;206&#039;&#039;&#039; || Teilantwort eines Datei-Downloads auf eine &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;-Anfrage; die Antwort enthält &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;416&#039;&#039;&#039; || Der angefragte &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt; liegt hinter dem Dateiende; die Antwort nennt in &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; die wirkliche Grösse&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;415&#039;&#039;&#039; || Datei-Upload an einen Endpunkt, der dafür nicht freigeschaltet ist (&amp;lt;code&amp;gt;re_upload = 0&amp;lt;/code&amp;gt;); oder ein &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt;, das der Server nicht entpacken kann - unterstützt wird allein &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt; (siehe [[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Kompression)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;413&#039;&#039;&#039; || Hochgeladene Datei überschreitet die zulässige Maximalgrösse (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;); oder der Anfragekörper überschreitet 10 MB bzw. - gepackt gesendet - das 400fache der gesendeten Grösse&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;429&#039;&#039;&#039; || Rate-Limit überschritten; die Antwort enthält den Header &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;503&#039;&#039;&#039; || Vier Ursachen, unterscheidbar am &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt;: &amp;lt;code&amp;gt;IDEMPOTENCY_IN_PROGRESS&amp;lt;/code&amp;gt; (eine Anfrage mit demselben &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird gerade verarbeitet), &amp;lt;code&amp;gt;SERVICE_UNAVAILABLE&amp;lt;/code&amp;gt; bei erreichter Andrangsgrenze des Server-Profils (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;), bei einer Anmeldung, deren Sitzungszeile nicht geschrieben werden konnte, sowie bei einem fehlgeschlagenen &amp;lt;code&amp;gt;JwtRevoke&amp;lt;/code&amp;gt;. Alle vier tragen &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;409&#039;&#039;&#039; || Ein früherer Aufruf mit demselben &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ist ohne Ergebnis geblieben (&amp;lt;code&amp;gt;IDEMPOTENCY_UNRESOLVED&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;422&#039;&#039;&#039; || Derselbe &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wurde mit &#039;&#039;&#039;anderem&#039;&#039;&#039; Inhalt gesendet (&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_REUSED&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;400&#039;&#039;&#039; || Ein schreibender Aufruf (POST/PUT/PATCH/DELETE) &#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;); ein resumable Upload ohne Dateinamen; oder eine Anfrage, die unverschlüsselt an einen TLS-Port geschickt wurde (&amp;lt;code&amp;gt;http&amp;lt;/code&amp;gt; statt &amp;lt;code&amp;gt;https&amp;lt;/code&amp;gt;) - letztere wird vor Authentifizierung und Endpunkt-Skript abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;308&#039;&#039;&#039; || Zwischenantwort beim resumable Upload: das Teilstück wurde angehängt, der Upload ist noch nicht vollständig. Die Antwort trägt &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;; das Skript läuft dabei &#039;&#039;&#039;nicht&#039;&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort besteht aus einem &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt;-Objekt mit&lt;br /&gt;
maschinenlesbarem Code - Aufbau siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].&lt;br /&gt;
&lt;br /&gt;
Daneben kann ein Endpunkt-Skript den Statuscode selbst setzen (z.B. &amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;409&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;) sowie Response-Header wie &amp;lt;code&amp;gt;ETag&amp;lt;/code&amp;gt; oder &amp;lt;code&amp;gt;Location&amp;lt;/code&amp;gt; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
== Idempotenz ==&lt;br /&gt;
&lt;br /&gt;
Bei &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;PUT&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;PATCH&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
sorgt der Server selbst dafür, dass eine wiederholte Sendung &#039;&#039;&#039;keine&lt;br /&gt;
Zweitwirkung&#039;&#039;&#039; hat. Das gilt für &#039;&#039;&#039;jeden&#039;&#039;&#039; Endpunkt - es muss weder am Endpunkt&lt;br /&gt;
etwas eingestellt noch im Skript etwas programmiert werden.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ist bei diesen vier Verben&lt;br /&gt;
Pflicht.&#039;&#039;&#039; Fehlt er, antwortet der Server mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; und das Endpunkt-Skript läuft &#039;&#039;&#039;nicht&#039;&#039;&#039; an.&lt;br /&gt;
&lt;br /&gt;
Bis v1.21 lief eine Anfrage ohne den Header ungeschützt durch; der Schutz gegen&lt;br /&gt;
die Doppelbuchung lag damit beim Client. Ausgenommen ist allein der&lt;br /&gt;
JWT-Endpunkt (Anmelden, Erneuern, Abmelden) - er greift vor der Adressauflösung&lt;br /&gt;
und kennt die Mechanik nicht.}}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Situation !! Antwort des Servers&lt;br /&gt;
|-&lt;br /&gt;
| Kein &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; gesendet || &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;. Das Skript läuft nicht&lt;br /&gt;
|-&lt;br /&gt;
| Erster Aufruf || Endpunkt läuft normal, das Ergebnis wird zum Schlüssel gespeichert&lt;br /&gt;
|-&lt;br /&gt;
| Wiederholung, gleicher Inhalt || &#039;&#039;&#039;Gespeicherte Antwort&#039;&#039;&#039; (gleicher Statuscode, gleicher Body, gleiche Header) plus Header &amp;lt;code&amp;gt;Idempotent-Replay: true&amp;lt;/code&amp;gt;. Das Skript läuft &#039;&#039;&#039;nicht&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Wiederholung, anderer Inhalt || &#039;&#039;&#039;422&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_REUSED&amp;lt;/code&amp;gt; - derselbe Schlüssel für einen anderen Inhalt ist ein Client-Fehler&lt;br /&gt;
|-&lt;br /&gt;
| Erster Aufruf läuft noch || &#039;&#039;&#039;503&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_IN_PROGRESS&amp;lt;/code&amp;gt; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;. Beim nächsten Versuch liegt die gespeicherte Antwort vor&lt;br /&gt;
|-&lt;br /&gt;
| Früherer Aufruf ohne Ergebnis || &#039;&#039;&#039;409&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_UNRESOLVED&amp;lt;/code&amp;gt;. Nur noch nach einem &#039;&#039;&#039;harten Abbruch&#039;&#039;&#039; des Dienstes zwischen Verarbeitung und Festschreiben; ob die Buchung stattgefunden hat, ist unbekannt. Der Aufräumlauf gibt solche Einträge nach 24 Stunden frei und protokolliert sie&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wichtig für die Auslegung eines Endpunkts:&lt;br /&gt;
&lt;br /&gt;
* Antwortet das Skript mit &#039;&#039;&#039;2xx&#039;&#039;&#039;, wird die Antwort gespeichert und bei einer Wiederholung erneut ausgeliefert.&lt;br /&gt;
* &#039;&#039;&#039;Ausnahme Dateiantwort:&#039;&#039;&#039; Liefert das Skript mit &amp;lt;code&amp;gt;SendFile&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;SendTempFile&amp;lt;/code&amp;gt; eine &#039;&#039;&#039;Datei&#039;&#039;&#039; aus, wird &#039;&#039;&#039;nichts&#039;&#039;&#039; festgeschrieben - eine Dateiantwort liesse sich nicht wiedergeben, gespeichert würde eine leere 200er-Antwort. Der Schlüssel wird stattdessen wieder freigegeben, ein erneuter Abruf derselben Datei ist unschädlich.&lt;br /&gt;
* Antwortet das Skript mit &#039;&#039;&#039;4xx&#039;&#039;&#039; (fachliche Ablehnung), wird der Schlüssel wieder &#039;&#039;&#039;freigegeben&#039;&#039;&#039; - der Konsument darf ihn nach Korrektur erneut verwenden. Sonst würde ein einziger Validierungsfehler den Schlüssel dauerhaft blockieren.&lt;br /&gt;
* Endet die Verarbeitung mit &#039;&#039;&#039;5xx&#039;&#039;&#039; oder einem Skript-Fehler, wird der Schlüssel &#039;&#039;&#039;freigegeben&#039;&#039;&#039; und der Fall protokolliert. Der Konsument darf denselben Schlüssel erneut senden.&lt;br /&gt;
* Das ist eine bewusste Abwägung (geändert 2026-08-25): Bleibt der Schlüssel nach einem Serverfehler belegt, ist der Vorgang &#039;&#039;&#039;unwiederholbar&#039;&#039;&#039; - der Client bekommt dauerhaft 503 und müsste die erfasste Arbeit verwerfen. Garantierter Datenverlust wiegt schwerer als ein möglicher Doppelsatz.&lt;br /&gt;
* &#039;&#039;&#039;Folge für die Auslegung:&#039;&#039;&#039; ein schreibender Endpunkt sollte selbst wiederholbar sein, etwa über ein fachliches Merkmal des Clients. Wo reines Anhängen stattfindet (Positionen, Dateien), bleibt sonst ein Restrisiko.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel gilt je &#039;&#039;&#039;Zugang, Methode und Pfad&#039;&#039;&#039;. Derselbe&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; an einem anderen Endpunkt ist damit ein eigener&lt;br /&gt;
Vorgang - der Konsument muss ihn nicht global eindeutig vergeben, aber pro Vorgang&lt;br /&gt;
&#039;&#039;&#039;stabil wiederverwenden&#039;&#039;&#039; (also nicht bei jedem Wiederholversuch neu erzeugen).&lt;br /&gt;
&lt;br /&gt;
Die Einträge stehen in &amp;lt;code&amp;gt;RESTSRV_IDEMPOTENCY&amp;lt;/code&amp;gt;. Abgeschlossene Einträge&lt;br /&gt;
werden nach &#039;&#039;&#039;30 Tagen&#039;&#039;&#039; gelöscht. Einträge &#039;&#039;&#039;ohne Ergebnis&#039;&#039;&#039; - sie entstehen&lt;br /&gt;
nur bei einem harten Abbruch des Dienstes - werden ab &#039;&#039;&#039;24 Stunden&#039;&#039;&#039; im Protokoll&lt;br /&gt;
gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039;. Das Stehenlassen wäre keine&lt;br /&gt;
Vorsicht, sondern eine Sperre: derselbe Vorgang mit demselben Schlüssel wäre sonst&lt;br /&gt;
nie wieder durchführbar.&lt;br /&gt;
&lt;br /&gt;
== WebHooks ==&lt;br /&gt;
&lt;br /&gt;
WebHooks sind &#039;&#039;&#039;Einmal-Endpunkte&#039;&#039;&#039; für asynchrone Callbacks. Beim ersten&lt;br /&gt;
erfolgreichen Aufruf wird die Antwort in der Tabelle &amp;lt;code&amp;gt;REMOTE_HOOK_URL&amp;lt;/code&amp;gt;&lt;br /&gt;
gespeichert und der Endpunkt anschließend automatisch gelöscht.&lt;br /&gt;
&lt;br /&gt;
Typischer Einsatz: OBS stösst einen Vorgang bei einem Fremdsystem an (Bezahldienst,&lt;br /&gt;
Versanddienstleister) und gibt diesem eine Rückruf-Adresse mit, die nur &#039;&#039;&#039;ein&lt;br /&gt;
einziges Mal&#039;&#039;&#039; gültig ist. Damit kann die Adresse nicht später erneut - oder von&lt;br /&gt;
jemand anderem - benutzt werden.&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# OBS legt einen Eintrag in &amp;lt;code&amp;gt;REMOTE_HOOK_URL&amp;lt;/code&amp;gt; an und dazu einen Endpunkt, dessen Feld &#039;&#039;&#039;WebHook&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_webhook&amp;lt;/code&amp;gt;) auf diesen Eintrag zeigt. Ein Skript wird für diesen Endpunkt nicht gepflegt.&lt;br /&gt;
# Die Adresse dieses Endpunkts wird dem Fremdsystem als Callback-URL übergeben.&lt;br /&gt;
# Das Fremdsystem ruft die Adresse auf. Der Server legt den übergebenen Inhalt in &amp;lt;code&amp;gt;REMOTE_HOOK_URL.hu_response&amp;lt;/code&amp;gt; ab, antwortet mit &amp;lt;code&amp;gt;{&amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;}&amp;lt;/code&amp;gt; und &#039;&#039;&#039;löscht den Endpunkt&#039;&#039;&#039;.&lt;br /&gt;
# Der weiterverarbeitende OBS-Prozess findet die Rückmeldung in &amp;lt;code&amp;gt;hu_response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein zweiter Aufruf derselben Adresse läuft ins Leere: Der Endpunkt&lt;br /&gt;
existiert nicht mehr, die Antwort ist 404. Das ist gewollt - ein WebHook ist keine&lt;br /&gt;
dauerhafte Schnittstelle. Für eine dauerhaft erreichbare Rückmelde-Adresse einen&lt;br /&gt;
normalen Endpunkt mit Skript anlegen.}}&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=65541</id>
		<title>OBS/Kostenpflichtige Module/RESTServer</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=65541"/>
		<updated>2026-09-25T09:28:32Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Idempotency-Key als Pflicht, Abschnitt Keep-alive, HSTS, vollstaendige Konsolenbefehle; Ablaufliste und Protokollverhalten berichtigt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
=REST-Server=&lt;br /&gt;
{{Hinweis|Es handelt sich um ein kostenpflichtiges Modul. Die Schnittstelle muss über den OBS-Support aktiviert werden.}}&lt;br /&gt;
&lt;br /&gt;
==Was leistet der REST-Server?==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server ist die &#039;&#039;&#039;universelle Schnittstelle&#039;&#039;&#039; Ihrer OBS-Installation nach außen. Er macht aus Ihrem OBS einen Dienst, mit dem andere Programme - Webseiten, Apps, Geräte, Kundensysteme, Cloud-Dienste - direkt sprechen können. Statt Daten manuell zu exportieren, zu mailen oder über Umwege bereitzustellen, holen sich angebundene Systeme genau die Information, die sie brauchen, in dem Moment, in dem sie sie brauchen - oder liefern neue Daten direkt in OBS ab.&lt;br /&gt;
&lt;br /&gt;
Für Anwender, die nicht selbst programmieren, bedeutet das: &#039;&#039;&#039;Was bisher nur manuell, per Datei-Import oder über Spezialschnittstellen ging, lässt sich jetzt automatisieren.&#039;&#039;&#039; Eine Webseite zeigt live aktuelle Lagerbestände. Mitarbeiter erfassen Zeiten über das Handy, ohne dass jemand Excel-Listen einpflegen muss. Ein Kunde stößt per Bestelltaste in seinem System einen Vorgang in Ihrem OBS an. Ein Lieferant meldet Wareneingänge automatisch zurück. Jeder dieser Anwendungsfälle wird einmal eingerichtet und läuft danach automatisch.&lt;br /&gt;
&lt;br /&gt;
Technisch gesehen ist der REST-Server ein in OBS integrierter, voll konfigurierbarer &#039;&#039;&#039;HTTP/HTTPS-Dienst&#039;&#039;&#039;, dessen Endpunkte über in OBS gepflegte Pascal-Skripte realisiert werden. Jeder Endpunkt bekommt eine eigene Adresse, eine eigene Logik und eine eigene Berechtigungsstruktur. Rückgaben erfolgen ausschliesslich im JSON-Format. Damit lassen sich beliebige Lese- und Schreibvorgänge auf der OBS-Datenbank realisieren, ohne dass externe Systeme direkten Datenbank-Zugriff erhalten - das ganze OBS-Regelwerk (Rechte, Validierung, Geschäftslogik) bleibt aktiv.&lt;br /&gt;
&lt;br /&gt;
Im Ergebnis ist der REST-Server kein einzelnes Feature, sondern ein &#039;&#039;&#039;Werkzeugkasten&#039;&#039;&#039;: Was an Daten oder Funktionen in OBS verfügbar ist, kann über den REST-Server auch nach außen angeboten - oder von außen entgegengenommen - werden. Die Bandbreite reicht vom einfachen Lese-Endpunkt für ein Webseiten-Widget bis hin zu kompletten B2B-Integrationen mit Mandanten- und Rollen-Trennung.&lt;br /&gt;
&lt;br /&gt;
Aufruf des Moduls in OBS:&lt;br /&gt;
&#039;&#039;&#039;Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;Z Weitere Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;REST-Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Anwendungsbereiche==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Einsatzfälle sind typische Beispiele, an denen sich der praktische Nutzen ablesen lässt. Es ist keine abschliessende Liste - alles, was sich in OBS-Skripten abbilden lässt, kann auch über einen Endpunkt bereitgestellt werden.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dies sind Beispiele. Für die konkrete Entwicklung können, je nach Komplexität, weitere Kosten anfallen.}}&lt;br /&gt;
&lt;br /&gt;
===Webseiten und Kundenportale===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Live-Daten auf der eigenen Webseite:&#039;&#039;&#039; Lagerbestände, Verfügbarkeiten, Preise, Auftragsstatus aus OBS direkt anzeigen, ohne tägliche Datei-Exports.&lt;br /&gt;
* &#039;&#039;&#039;Kundenportale:&#039;&#039;&#039; Kunden sehen ihre offenen Posten, Auftragshistorie, Lieferscheine. Die Seite holt sich die Daten zur Anzeigezeit direkt aus OBS, jeder Kunde sieht nur seine eigenen Daten.&lt;br /&gt;
* &#039;&#039;&#039;Trackingseiten:&#039;&#039;&#039; Status einer Bestellung, eines Auftrags oder einer Reparatur für Endkunden, abrufbar über eine Sendungsnummer.&lt;br /&gt;
&lt;br /&gt;
===Mobile Anwendungen und Außendienst===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Mobile Zeiterfassung:&#039;&#039;&#039; Mitarbeiter buchen Anfang, Pause und Ende über Smartphone oder Tablet, die Daten landen direkt in OBS - ohne Zettelwirtschaft.&lt;br /&gt;
* &#039;&#039;&#039;QR-/Barcode-Scanner und Lagergeräte:&#039;&#039;&#039; Wareneingang, Inventur, Kommissionierung direkt in OBS abbilden, ohne zwischengeschaltete Software.&lt;br /&gt;
* &#039;&#039;&#039;Außendienst-Apps:&#039;&#039;&#039; Techniker sehen ihre Aufträge, dokumentieren Einsätze, erfassen Material - synchronisiert mit OBS.&lt;br /&gt;
&lt;br /&gt;
===Anbindung von Drittsystemen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Webshops:&#039;&#039;&#039; Bestellungen werden direkt vom Shop in OBS gemeldet, Lagerbestände und Preise vom Shop bei OBS abgefragt.&lt;br /&gt;
* &#039;&#039;&#039;Buchhaltungs- und ERP-Systeme:&#039;&#039;&#039; Bidirektionaler Austausch von Belegen, Stammdaten und Buchungen.&lt;br /&gt;
* &#039;&#039;&#039;CRM- und Marketing-Tools:&#039;&#039;&#039; Personendaten, Aktivitäten oder Mailing-Empfänger werden synchron gehalten.&lt;br /&gt;
* &#039;&#039;&#039;Logistikdienstleister:&#039;&#039;&#039; Versandaufträge werden übergeben, Tracking-Informationen zurückgeliefert.&lt;br /&gt;
&lt;br /&gt;
===Automatisierte Rückmeldungen (WebHooks)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zahlungs-Callbacks:&#039;&#039;&#039; Bezahldienste (z.B. PayPal, Klarna, Stripe) melden den Zahlungseingang an einen WebHook-Endpunkt - OBS bucht automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Versanddienste:&#039;&#039;&#039; Statusmeldungen (verschickt, zugestellt, retour) werden direkt im passenden Vorgang dokumentiert.&lt;br /&gt;
* &#039;&#039;&#039;Cloud-Workflows:&#039;&#039;&#039; Externe Automatisierungs-Plattformen (Make, Zapier, n8n, ...) stoßen OBS-Vorgänge an oder werden von OBS aus angesprochen.&lt;br /&gt;
&lt;br /&gt;
===Geschäftspartner-Kommunikation (B2B)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lieferanten-Schnittstelle:&#039;&#039;&#039; Bestellungen werden automatisch übergeben, Auftragsbestätigungen und Lieferavise zurückgespielt.&lt;br /&gt;
* &#039;&#039;&#039;Kunden-Schnittstelle:&#039;&#039;&#039; Grosskunden bestellen direkt aus ihrem ERP heraus, Stammdaten und Konditionen werden zentral gepflegt.&lt;br /&gt;
* &#039;&#039;&#039;Sichere Maschine-zu-Maschine-Kommunikation:&#039;&#039;&#039; Mit Mutual-TLS (mTLS) wird sichergestellt, dass nur die freigeschalteten Partnersysteme - und niemand sonst - mit OBS sprechen können.&lt;br /&gt;
&lt;br /&gt;
===Interne Microservices und Automatisierungen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eigene kleine Hilfs-Tools:&#039;&#039;&#039; Zeitschaltautomatik, Reporting-Generator, Sammelaktionen, die aus mehreren Quellen Daten in OBS verarbeiten.&lt;br /&gt;
* &#039;&#039;&#039;Verbindung mehrerer Standorte:&#039;&#039;&#039; Verteilte Systeme tauschen Daten über definierte Endpunkte aus, ohne offene Datenbankverbindungen.&lt;br /&gt;
* &#039;&#039;&#039;Datenbereitstellung für Dashboards und Auswertungen:&#039;&#039;&#039; BI-Tools (Power BI, Grafana, ...) lesen aufbereitete Kennzahlen direkt aus OBS.&lt;br /&gt;
&lt;br /&gt;
===Was bedeutet das wirtschaftlich?===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Manuelle Arbeit fällt weg.&#039;&#039;&#039; Daten werden nicht mehr aus OBS exportiert, per Mail geschickt und woanders eingelesen - sie fliessen direkt.&lt;br /&gt;
* &#039;&#039;&#039;Fehler werden weniger.&#039;&#039;&#039; Jede manuelle Stelle ist eine mögliche Fehlerquelle; jede Automatisierung eine Stelle, an der nichts mehr schiefgehen kann.&lt;br /&gt;
* &#039;&#039;&#039;Neue Geschäftsmodelle werden möglich.&#039;&#039;&#039; Kunden- und Lieferantenportale, Self-Service-Funktionen, App-Anbindungen lassen sich aufbauen, ohne ein zweites System pflegen zu müssen.&lt;br /&gt;
* &#039;&#039;&#039;Bestehende Software bleibt verbunden.&#039;&#039;&#039; Statt eine vorhandene Anwendung ablösen zu müssen, kann sie über den REST-Server an OBS angedockt werden - in beiden Richtungen.&lt;br /&gt;
* &#039;&#039;&#039;Skaliert mit:&#039;&#039;&#039; Was klein anfängt (ein einzelner Endpunkt für eine Webseite) kann zu einer kompletten Integrations-Plattform ausgebaut werden, ohne dass die Grundlage gewechselt werden muss.&lt;br /&gt;
&lt;br /&gt;
==Architektur im Überblick==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server besteht aus mehreren aufeinander aufbauenden Bausteinen, die alle in OBS gepflegt werden:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Baustein     !! Tabelle              !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| Server       || RESTSRV_SERVER       || TLS-Profil + eigene HTTP-Server-Instanz, kann mehrfach existieren&lt;br /&gt;
|-&lt;br /&gt;
| Bindung      || RESTSRV_BINDINGS     || IP-Adresse + Port pro Server&lt;br /&gt;
|-&lt;br /&gt;
| Zugang       || RESTSRV_ACCOUNT      || API-Key, optionale Host- und CORS-Beschränkung, optionale JWT-Konfiguration&lt;br /&gt;
|-&lt;br /&gt;
| Endpunkt     || RESTSRV_ENDPOINTS    || Skript, das die Anfrage bearbeitet, einem Server fest zugeordnet&lt;br /&gt;
|-&lt;br /&gt;
| Berechtigung || RESTSRV_ACCESS       || Verbindet Zugang und Endpunkt&lt;br /&gt;
|-&lt;br /&gt;
| Idempotenz   || RESTSRV_IDEMPOTENCY  || Speicher für Idempotency-Keys und die dazu gelieferte Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Sitzung      || RESTSRV_TOKEN        || Ausgestellte Token-Paare je Sitzung; Grundlage für Rotation und Widerruf&lt;br /&gt;
|-&lt;br /&gt;
| Protokoll    || RESTSRV_PROTO        || Laufende Ereignisse, Fehler, Audit-Einträge&lt;br /&gt;
|-&lt;br /&gt;
| Statistik    || RESTSRV_STATS        || Aufrufzähler, Antwortzeiten und HTTP-Status pro Endpunkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Eine Anfrage durchläuft folgende Stationen:&lt;br /&gt;
&lt;br /&gt;
# Rate-Limit-Prüfung (pro API-Key bzw. IP und global, Schwellen je Server-Profil)&lt;br /&gt;
# Andrangsgrenze des Server-Profils (&#039;&#039;Max. parallel&#039;&#039;) - bei Überschreitung &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039;&lt;br /&gt;
# Authentifizierung über den API-Key (Header &#039;&#039;apikey&#039;&#039;)&lt;br /&gt;
# CORS-Prüfung (sofern &#039;&#039;Origin&#039;&#039;-Header gesetzt)&lt;br /&gt;
# Optional: JWT-Prüfung - Signatur, Ablauf und Sitzung (sofern für den Zugang aktiviert)&lt;br /&gt;
# Routing: Auflösung des Pfads über die Pfad-Templates der Endpunkte&lt;br /&gt;
# Prüfung der Berechtigung (Zugang -&amp;gt; Endpunkt)&lt;br /&gt;
# Entpacken des Anfragekörpers, sofern er mit &#039;&#039;Content-Encoding: gzip&#039;&#039; gesendet wurde&lt;br /&gt;
# Idempotenz-Prüfung bei POST/PUT/PATCH/DELETE - der Header &#039;&#039;Idempotency-Key&#039;&#039; ist dort &#039;&#039;&#039;Pflicht&#039;&#039;&#039;&lt;br /&gt;
# Ausführung des Endpunkt-Skripts (oder WebHook-Antwort)&lt;br /&gt;
# Festschreiben des Ergebnisses im Idempotenz-Speicher (sofern in Schritt 9 reserviert)&lt;br /&gt;
# JSON-Antwort und Statistik-Eintrag&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Reihenfolge ist kein Zufall.&#039;&#039;&#039; Das Entpacken liegt &#039;&#039;&#039;hinter&#039;&#039;&#039;&lt;br /&gt;
der Authentifizierung, damit ein Unbefugter dem Server keine Rechenzeit für eine&lt;br /&gt;
Dekompressionsbombe abverlangen kann - und &#039;&#039;&#039;vor&#039;&#039;&#039; der Idempotenz-Reservierung,&lt;br /&gt;
damit eine abgewiesene Anfrage keinen &#039;&#039;Idempotency-Key&#039;&#039; verbrennt.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Der Normalfall erzeugt keine Protokollzeile.&#039;&#039;&#039; Bis 08/2026 schrieb jede&lt;br /&gt;
Anfrage eine Zeile nach RESTSRV_PROTO; sie ist entfallen, weil RESTSRV_STATS&lt;br /&gt;
zu derselben Anfrage IP, Zugang, Endpunkt, Methode, Pfad, Status, Laufzeit und&lt;br /&gt;
dieselbe Korrelations-ID trägt - und länger aufbewahrt wird. Das Protokoll trägt&lt;br /&gt;
seitdem nur noch Auffälligkeiten.}}&lt;br /&gt;
&lt;br /&gt;
==Adressierung==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt wird über ein &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; angesprochen, das den Pfad nach dem Host beschreibt und Platzhalter enthalten kann:&lt;br /&gt;
&lt;br /&gt;
 http://[Hostadresse][:Port][Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
&lt;br /&gt;
 https://api.meinserver.de/kalender/v1&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}/modules/{code}&lt;br /&gt;
&lt;br /&gt;
Statische Segmente müssen exakt passen; Platzhalter in geschweiften Klammern (z.B. {uid}) matchen einen beliebigen Wert und stehen im Skript als Pfad-Parameter zur Verfügung. Eine Versionskennung wird als statisches Segment ins Template aufgenommen (z.B. /kalender/v1), um eine Funktionalität zu verändern, ohne bestehende Konsumenten zu brechen. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==Unterstützte HTTP-Methoden==&lt;br /&gt;
&lt;br /&gt;
Der Server akzeptiert die Methoden &#039;&#039;&#039;GET&#039;&#039;&#039;, &#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;DELETE&#039;&#039;&#039; und &#039;&#039;&#039;PATCH&#039;&#039;&#039; sowie &#039;&#039;&#039;OPTIONS&#039;&#039;&#039; (CORS-Preflight, ohne Skript-Aufruf). Jede Methode wird im Endpunkt-Skript als gleichnamige Prozedur implementiert - &#039;&#039;&#039;mit einer Ausnahme&#039;&#039;&#039;: &#039;&#039;Delete&#039;&#039; ist in der Skriptsprache ein reservierter Name und lässt sich nicht als Prozedur deklarieren. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Prozedur, antwortet der Server mit &#039;&#039;&#039;405&#039;&#039;&#039; und&lt;br /&gt;
nennt im Header &#039;&#039;Allow&#039;&#039; die Verben, die dieses Skript tatsächlich anbietet.&lt;br /&gt;
&lt;br /&gt;
==Kompression==&lt;br /&gt;
&lt;br /&gt;
Der Server packt Antworten mit &#039;&#039;&#039;gzip&#039;&#039;&#039;, wenn der Client sie annimmt. Ausgehandelt wird über den Anfrage-Header &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt;; die Antwort trägt dann &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Vary: Accept-Encoding&amp;lt;/code&amp;gt; - bei einer CORS-Anfrage steht dort &amp;lt;code&amp;gt;Vary: Origin, Accept-Encoding&amp;lt;/code&amp;gt;, also &#039;&#039;&#039;eine&#039;&#039;&#039; Zeile mit beiden Bedingungen. Browser, &amp;lt;code&amp;gt;curl --compressed&amp;lt;/code&amp;gt; und die gängigen HTTP-Bibliotheken entpacken selbstständig - für den Konsumenten ändert sich nichts ausser der übertragenen Datenmenge.&lt;br /&gt;
&lt;br /&gt;
Gepackt wird nur, wo es etwas bringt:&lt;br /&gt;
&lt;br /&gt;
* Nur JSON (auch &#039;&#039;problem+json&#039;&#039; und &#039;&#039;ndjson&#039;&#039;), XML, JavaScript, SVG und alles unter &#039;&#039;text/&#039;&#039; - darunter Text, CSV und HTML. Bereits komprimierte Typen - PDF, Bild, ZIP, Video - werden durch gzip &#039;&#039;&#039;grösser&#039;&#039;&#039;, nicht kleiner.&lt;br /&gt;
* Erst ab &#039;&#039;&#039;1024 Byte&#039;&#039;&#039;. Darunter kostet der gzip-Rahmen mehr, als der Inhalt einspart.&lt;br /&gt;
* &#039;&#039;&#039;Nicht&#039;&#039;&#039; bei Dateiantworten, nicht bei Teilauslieferungen (206) und nicht bei Antworten ohne Körper (204, 304).&lt;br /&gt;
* Nicht, wenn das Endpunkt-Skript den Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; selbst gesetzt hat - dann gehört die Antwort dem Skript.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;deflate&#039;&#039;&#039; wird in &#039;&#039;&#039;keiner&#039;&#039;&#039; Richtung unterstützt. Der Wert ist historisch uneindeutig - die einen senden RFC-1950-gerahmt, die anderen roh -, und wer ihn nennt, beherrscht ohnehin gzip. Eine Anfrage mit &amp;lt;code&amp;gt;Content-Encoding: deflate&amp;lt;/code&amp;gt; wird mit 415 abgewiesen.&lt;br /&gt;
&lt;br /&gt;
Abschalten lässt sich das Packen je Server-Profil über den Haken &#039;&#039;Keine Kompression&#039;&#039; (siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]). Der Haken betrifft ausschliesslich die Antwortrichtung.&lt;br /&gt;
&lt;br /&gt;
===Komprimierte Anfragen===&lt;br /&gt;
&lt;br /&gt;
In der Gegenrichtung nimmt der Server &#039;&#039;&#039;gzip&#039;&#039;&#039; entgegen: Trägt eine Anfrage den Header &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt;, wird der Körper entpackt, bevor ihn jemand liest. Das Endpunkt-Skript merkt davon nichts.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Anfrage                                              !! Antwort&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; fehlt oder &amp;lt;code&amp;gt;identity&amp;lt;/code&amp;gt; || normale Verarbeitung; ab 10 MB Körpergrösse &#039;&#039;&#039;413&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt;                                    || Körper wird entpackt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;deflate&amp;lt;/code&amp;gt;, anderer Wert (z.B. &amp;lt;code&amp;gt;br&amp;lt;/code&amp;gt;) oder mehrere Werte || &#039;&#039;&#039;415&#039;&#039;&#039; &amp;lt;code&amp;gt;UNSUPPORTED_MEDIA_TYPE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Gepackter Körper bei einem Datei-Upload              || &#039;&#039;&#039;415&#039;&#039;&#039; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über &#039;&#039;&#039;10 MB&#039;&#039;&#039;                    || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über dem &#039;&#039;&#039;400fachen&#039;&#039;&#039; der gesendeten Grösse || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für einen gepackten Anfragekörper gelten &#039;&#039;&#039;zwei&#039;&#039;&#039; Grenzen. Beide zählen den &#039;&#039;&#039;entpackten&#039;&#039;&#039; Inhalt, und beide greifen bereits &#039;&#039;&#039;während&#039;&#039;&#039; des Entpackens - ein Körper, der sie reisst, belegt den Speicher gar nicht erst:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;10 MB&#039;&#039;&#039; absolut. Dieselbe Grenze, die auch für einen ungepackten Körper gilt.&lt;br /&gt;
* &#039;&#039;&#039;das 400fache&#039;&#039;&#039; der gesendeten Grösse, &#039;&#039;&#039;mindestens aber 64 KB&#039;&#039;&#039;. Damit wird eine &#039;&#039;Dekompressionsbombe&#039;&#039; - wenige Kilobyte, die sich auf Hunderte Megabyte aufblähen - schon nach wenigen Prozent abgebrochen. Der Boden von 64 KB sorgt dafür, dass die Verhältnisrechnung winzige Körper gar nicht erst trifft: ein 100-Byte-Körper darf sich auf 64 KB entpacken, nicht nur auf 40 KB.&lt;br /&gt;
&lt;br /&gt;
Die zweite Grenze trifft normale Nutzlasten nicht. Gemessen erreichen echte JSON-Körper Verhältnisse von 24:1 bis 40:1, und selbst eine Liste aus zehntausenden identischen Zeilen kommt nur auf rund 340:1 - mehr gibt das Verfahren für wiederholte Datensätze nicht her. Über 400:1 kommt nur, wer sehr lange Läufe desselben Zeichens sendet.&lt;br /&gt;
&lt;br /&gt;
Für Datei-Uploads gilt keine der beiden Grenzen - dort zählt allein die je Endpunkt eingestellte Maximalgrösse.}}&lt;br /&gt;
&lt;br /&gt;
==Persistente Verbindungen (Keep-alive)==&lt;br /&gt;
&lt;br /&gt;
Der Server hält eine Verbindung nach der Antwort &#039;&#039;&#039;offen&#039;&#039;&#039;, statt sie zu&lt;br /&gt;
schliessen. Ein Client, der mehrere Anfragen hintereinander stellt - eine App,&lt;br /&gt;
die eine Liste seitenweise abholt -, spart damit je Folgeanfrage den Aufbau von&lt;br /&gt;
TCP-Verbindung und TLS-Sitzung.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Grenze !! Wert !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Leerlauf || &#039;&#039;&#039;10 Sekunden&#039;&#039;&#039; || Passiert auf der Verbindung so lange nichts, wird sie geschlossen&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen je Verbindung || &#039;&#039;&#039;100&#039;&#039;&#039; || Danach schliesst der Server geordnet, der Client baut neu auf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beides ist für den Konsumenten unkritisch: Eine geschlossene Verbindung ist kein&lt;br /&gt;
Fehler, jede HTTP-Bibliothek baut sie bei der nächsten Anfrage selbst wieder auf.&lt;br /&gt;
Wer viele Anfragen hintereinander stellt, gewinnt aber deutlich, wenn er seinen&lt;br /&gt;
Client die Verbindung wiederverwenden lässt (&#039;&#039;Connection: keep-alive&#039;&#039; ist der&lt;br /&gt;
Standard von HTTP/1.1, die meisten Bibliotheken tun es von allein).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Leerlaufgrenze gilt für jeden Lesevorgang der Verbindung&#039;&#039;&#039;,&lt;br /&gt;
nicht nur für das Warten auf die nächste Anfrage. Ein Client, der seinen&lt;br /&gt;
Anfragekörper nur tropfenweise sendet und dazwischen länger als 10 Sekunden&lt;br /&gt;
pausiert, verliert die Verbindung mitten im Senden.}}&lt;br /&gt;
&lt;br /&gt;
Die Werte sind bewusst knapp gewählt: Der Server fährt einen eigenen Thread je&lt;br /&gt;
Verbindung, eine leerlaufende Verbindung kostet also nicht bloss einen&lt;br /&gt;
Dateizeiger. Sie sind keine Lastgrenze - dafür gibt es &#039;&#039;Max. Verbindungen&#039;&#039; und&lt;br /&gt;
&#039;&#039;Max. parallel&#039;&#039; am Server-Profil (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]).&lt;br /&gt;
&lt;br /&gt;
==Datei-Uploads==&lt;br /&gt;
&lt;br /&gt;
Endpunkte, die dafür freigeschaltet sind (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;), nehmen Dateien&lt;br /&gt;
per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; oder als fortsetzbaren &#039;&#039;&#039;Resumable-Upload&#039;&#039;&#039;&lt;br /&gt;
(&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;) entgegen. Die maximale Grösse wird pro Endpunkt&lt;br /&gt;
festgelegt (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB); Überschreitung&lt;br /&gt;
ergibt 413, ein Upload an einen nicht freigeschalteten Endpunkt 415. Der Server&lt;br /&gt;
legt die Datei temporär ab und übergibt dem Skript den Pfad; die weitere&lt;br /&gt;
Verarbeitung (z.B. DMS-Ablage) übernimmt das Skript. Details:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
==Doppelte Sendungen abfangen (Idempotenz)==&lt;br /&gt;
&lt;br /&gt;
Ein Client, der nach einem Timeout dieselbe Anfrage erneut schickt, darf keine&lt;br /&gt;
Zweitbuchung auslösen. Der Server erledigt das selbst: Jeder Aufruf mit&lt;br /&gt;
&#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;PATCH&#039;&#039;&#039; oder &#039;&#039;&#039;DELETE&#039;&#039;&#039; muss den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; tragen; der Server merkt sich Schlüssel und Ergebnis&lt;br /&gt;
in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;. Eine Wiederholung mit demselben Schlüssel und&lt;br /&gt;
demselben Inhalt bekommt die &#039;&#039;&#039;gespeicherte Antwort&#039;&#039;&#039; zurück - das&lt;br /&gt;
Endpunkt-Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header ist Pflicht, nicht optional.&#039;&#039;&#039; Ein schreibender Aufruf&lt;br /&gt;
&#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; abgewiesen, und das Endpunkt-Skript läuft&lt;br /&gt;
nicht an.&lt;br /&gt;
&lt;br /&gt;
Das war bis v1.21 anders: fehlte der Header, lief die Anfrage ungeschützt durch.&lt;br /&gt;
Der Schutz gegen eine Doppelbuchung stand damit in der Zusage des Clients und&lt;br /&gt;
nicht in einer Prüfung des Servers - ein Wiederholversuch nach Zeitüberschreitung&lt;br /&gt;
buchte ein zweites Mal. Konsumenten, die bisher ohne den Header schreiben, müssen&lt;br /&gt;
angepasst werden.}}&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Pflicht nur für schreibende Verben.&#039;&#039;&#039; &#039;&#039;GET&#039;&#039; braucht keinen Schlüssel und kennt die Mechanik nicht.&lt;br /&gt;
* &#039;&#039;&#039;Der JWT-Endpunkt ist ausgenommen.&#039;&#039;&#039; Anmelden, Erneuern und Abmelden laufen vor der Adressauflösung und brauchen keinen Schlüssel.&lt;br /&gt;
* &#039;&#039;&#039;Kein Schalter am Endpunkt.&#039;&#039;&#039; Die Mechanik greift für jeden Endpunkt automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Das Skript muss nichts tun.&#039;&#039;&#039; Eine eigene Idempotenz-Logik im Skript ist nicht nötig.&lt;br /&gt;
* &#039;&#039;&#039;Pro Vorgang ein Schlüssel&#039;&#039;&#039;, über alle Wiederholversuche hinweg unverändert - nicht bei jedem Versuch neu erzeugt.&lt;br /&gt;
&lt;br /&gt;
Details und die Statuscodes: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen und Abmelden==&lt;br /&gt;
&lt;br /&gt;
Ein JWT trägt seine Gültigkeit in sich: der Server prüft Signatur und Ablauf und&lt;br /&gt;
braucht dafür keine Datenbank. Genau deshalb liesse sich ein ausgegebener Token&lt;br /&gt;
von sich aus auch nicht mehr zurückziehen - ein verlorenes Gerät wäre bis zum&lt;br /&gt;
Ablauf des Tokens weiter nutzbar.&lt;br /&gt;
&lt;br /&gt;
Der Server führt deshalb zu jedem ausgestellten Token-Paar eine Zeile in&lt;br /&gt;
&#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und prüft jeden Zugriff dagegen. Daraus ergeben sich drei&lt;br /&gt;
Eigenschaften:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Abmelden wirkt sofort.&#039;&#039;&#039; Ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt sperrt die Sitzung; alle Token dieser Sitzung sind damit unmittelbar ungültig, auch die noch nicht abgelaufenen. Anmelden, Erneuern und Abmelden laufen also über einen einzigen Endpunkt, und für das Abmelden ist kein Skript nötig.&lt;br /&gt;
* &#039;&#039;&#039;Ein Refresh-Token gilt genau einmal.&#039;&#039;&#039; Beim Erneuern wird es entwertet. Wird es später erneut vorgelegt, gilt das als Diebstahl oder Fehlfunktion: die betroffene Sitzung wird komplett gesperrt und der Vorfall protokolliert.&lt;br /&gt;
* &#039;&#039;&#039;Alle Geräte abmelden.&#039;&#039;&#039; Ein gewöhnlicher Endpunkt kann über den Aufruf &#039;&#039;oWriter.JwtRevoke(&#039;all&#039;)&#039;&#039; auch alle Sitzungen eines Benutzers sperren - der Support-Fall „Gerät verloren&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Mit der Einführung dieser Prüfung verlieren &#039;&#039;&#039;alle vorher&lt;br /&gt;
ausgestellten Token&#039;&#039;&#039; ihre Gültigkeit, weil sie zu keiner Sitzung gehören.&lt;br /&gt;
Angemeldete Konsumenten müssen sich nach dem Update einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
Der Speicher pflegt sich selbst: Zeilen werden gelöscht, sobald sowohl das&lt;br /&gt;
Access- als auch das Refresh-Token abgelaufen sind. Details und die Skript-Seite:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Fehlerformat==&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort des Servers hat dieselbe Form: ein einziges &#039;&#039;error&#039;&#039;-Objekt&lt;br /&gt;
mit maschinenlesbarem Code, Support-Kennung, Meldung und Korrelations-ID:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00E2RTPDY&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Ungültiges oder abgelaufenes Token&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260817T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;code&#039;&#039;&#039; - sprechender Bezeichner (z.B. &#039;&#039;AUTH_EXPIRED&#039;&#039;, &#039;&#039;NOT_FOUND&#039;&#039;, &#039;&#039;RATE_LIMITED&#039;&#039;, &#039;&#039;INTERNAL_ERROR&#039;&#039;), an dem ein Client seine Reaktion festmachen kann, ohne Texte auszuwerten.&lt;br /&gt;
* &#039;&#039;&#039;uid&#039;&#039;&#039; - OBS-Kennung der Codestelle. Für die Support-Recherche gedacht, nicht zur Auswertung im Client.&lt;br /&gt;
* &#039;&#039;&#039;message&#039;&#039;&#039; - Text für den Client. Interne Fehlerdetails stehen nie darin, sondern ausschliesslich im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;traceId&#039;&#039;&#039; - Korrelations-ID. Steht zusätzlich im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Logzeile.&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt-Skript kann eigene Codes setzen; siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;Fehlercode&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; und &#039;&#039;traceId&#039;&#039; stehen &#039;&#039;&#039;nur&#039;&#039;&#039; im&lt;br /&gt;
&#039;&#039;error&#039;&#039;-Objekt, nicht flach daneben - der Wert von&lt;br /&gt;
&#039;&#039;Fehlercode&#039;&#039; heisst jetzt &#039;&#039;error.uid&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; entspricht&lt;br /&gt;
&#039;&#039;error.message&#039;&#039;. Clients, die die flachen Felder auswerten, müssen angepasst&lt;br /&gt;
werden.}}&lt;br /&gt;
&lt;br /&gt;
==Sicherheitsmerkmale==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server enthält eine Reihe von Schutzmechanismen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rate-Limit:&#039;&#039;&#039; Standardmässig max. 10 fehlgeschlagene und 120 erfolgreiche Anfragen pro API-Key (Account) bzw. IP und 60 Sekunden, max. 600 Anfragen global pro 60 Sekunden. Beim Überschreiten wird HTTP 429 mit &#039;&#039;Retry-After&#039;&#039;-Header zurückgegeben. Die vier Schwellen lassen sich &#039;&#039;&#039;pro Server-Profil&#039;&#039;&#039; hinterlegen; die Zähler laufen ebenfalls getrennt je Profil, ein überlastetes Profil bremst das andere also nicht mit aus. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
* &#039;&#039;&#039;Body-Limit:&#039;&#039;&#039; max. 10 MB Request-Body (JSON), max. 1024 Zeichen pro Header- oder Query-Parameter. Datei-Uploads laufen über einen separaten Pfad und sind pro Endpunkt auf &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; MB begrenzt (Standard 25&amp;amp;nbsp;MB).&lt;br /&gt;
* &#039;&#039;&#039;Sensible Felder&#039;&#039;&#039; (&#039;&#039;password&#039;&#039;, &#039;&#039;token&#039;&#039;, &#039;&#039;secret&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, ...) werden in Protokoll-Ausgaben automatisch maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Token:&#039;&#039;&#039; Jedes ausgestellte Token-Paar gehört zu einer Sitzung in &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und wird bei &#039;&#039;&#039;jedem&#039;&#039;&#039; Zugriff dagegen geprüft. Eine Abmeldung wirkt damit sofort und nicht erst mit dem Ablauf des Tokens; ein Refresh-Token gilt genau einmal. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
* &#039;&#039;&#039;Reserviertes Präfix:&#039;&#039;&#039; Parameter-Namen mit Präfix &#039;&#039;_OBS_&#039;&#039; können von aussen nicht gesetzt werden, sie sind für den Server reserviert (z.B. JWT-Claims).&lt;br /&gt;
* &#039;&#039;&#039;Geblockte Header:&#039;&#039;&#039; &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039; werden nicht an das Skript durchgereicht.&lt;br /&gt;
* &#039;&#039;&#039;TLS:&#039;&#039;&#039; Bei Profilen mit aktivem TLS fährt der Server ohne Zertifikat und Key nicht hoch. Plain-HTTP ist nur für ein dediziertes Debug-Profil möglich.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Antworten eines TLS-Profils tragen &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse danach ein Jahr lang nur noch über HTTPS an. Beim Debug-Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;mTLS&#039;&#039;&#039; (Mutual TLS): erzwingt, dass jeder Client ein gültiges Client-Zertifikat vorlegt. Optional kann pro Zugang ein erwarteter Subject-DN hinterlegt werden.&lt;br /&gt;
* &#039;&#039;&#039;DNS-Cache:&#039;&#039;&#039; Host-zu-IP-Auflösungen für die Zugangs-Host-Prüfung werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
==Protokoll==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; enthält alle relevanten Ereignisse: erfolgreiche und abgelehnte Anfragen, TLS-/JWT-Fehler, Bindungs-Ereignisse, Skript-Fehler. Einträge sind nach Datum, Uhrzeit und Host (IP oder Account-Name) sortiert. Das Protokoll ist die erste Anlaufstelle bei Störungen. Jede Antwort trägt zudem eine Korrelations-ID (Response-Header &#039;&#039;X-Trace-Id&#039;&#039;), die in den Protokoll-Einträgen wiederzufinden ist.&lt;br /&gt;
&lt;br /&gt;
==Statistik==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_STATS&#039;&#039;&#039; enthält eine Statistik aller bearbeiteten Anfragen&lt;br /&gt;
mit IP, Zugang, Endpunkt, Methode, angefragtem Pfad, HTTP-Status und Laufzeit in&lt;br /&gt;
Millisekunden. Dazu kommen zwei Felder, die den Sprung ins Protokoll erlauben:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;rs_trace&#039;&#039;&#039; - dieselbe Korrelations-ID, die auch im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Protokollzeile steht. Damit findet man zu einer auffälligen Statistikzeile alle Protokolleinträge derselben Anfrage.&lt;br /&gt;
* &#039;&#039;&#039;rs_server&#039;&#039;&#039; - das Server-Profil, über das die Anfrage hereinkam. Bei mehreren Profilen (Public-TLS neben Internal-mTLS) ist das die einzige Angabe, an der sich der Weg ablesen lässt.&lt;br /&gt;
&lt;br /&gt;
Weil der Normalfall keine Protokollzeile mehr schreibt, ist RESTSRV_STATS die&lt;br /&gt;
vollständige Sicht auf den Betrieb - RESTSRV_PROTO die auf die Auffälligkeiten.&lt;br /&gt;
&lt;br /&gt;
Aufruf der Statistik:&lt;br /&gt;
&lt;br /&gt;
* Aus der Endpunkt-Liste: F8 zeigt die Statistik des markierten Endpunkts.&lt;br /&gt;
* Aus der Zugänge-Liste: F8 zeigt die Statistik des markierten Zugangs.&lt;br /&gt;
&lt;br /&gt;
==Konsole==&lt;br /&gt;
&lt;br /&gt;
Wird der REST-Server nicht als Dienst, sondern interaktiv gestartet, erscheint eine&lt;br /&gt;
farbige Konsole mit Live-Log und einem &#039;&#039;&#039;fixierten Kopfbereich&#039;&#039;&#039;, der die gerade&lt;br /&gt;
laufenden Anfragen zeigt und sich fortlaufend aktualisiert.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;show &amp;lt;handle&amp;gt;&#039;&#039; || Details einer Anfrage: Request-Header, Protokollzeilen, Antwort. JSON wird farbig formatiert. &#039;&#039;&amp;lt;handle&amp;gt;&#039;&#039; ist die Kennung aus der Abschlusszeile der Anfrage (z.B. &#039;&#039;7F&#039;&#039;), &#039;&#039;&#039;keine&#039;&#039;&#039; laufende Nummer&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;1&#039;&#039; … &#039;&#039;9&#039;&#039; || Auswahl im Kopfbereich - kürzer als &#039;&#039;show&#039;&#039; für die gerade sichtbaren Anfragen. Mit &#039;&#039;mouse on&#039;&#039; auch per Klick&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;ps&#039;&#039; || Listet die gerade laufenden Anfragen auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;level &amp;lt;stufe&amp;gt;&#039;&#039; || Anzeigestufe des Live-Logs: &#039;&#039;quiet&#039;&#039; , &#039;&#039;normal&#039;&#039; oder &#039;&#039;debug&#039;&#039;. Ohne Parameter wird die aktuelle Stufe genannt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;filter &amp;lt;ausdruck&amp;gt;&#039;&#039; || Blendet aus, was nicht passt: &#039;&#039;path=&amp;lt;text&amp;gt;&#039;&#039;, &#039;&#039;account=&amp;lt;name&amp;gt;&#039;&#039;, &#039;&#039;status&amp;gt;=&amp;lt;code&amp;gt;&#039;&#039;. &#039;&#039;filter aus&#039;&#039; hebt die Einschränkung auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;panel on&amp;amp;#124;off&#039;&#039; || Fixierten Kopfbereich ein-/ausschalten. &#039;&#039;&#039;Ausgeschaltet&#039;&#039;&#039; lässt sich der Rückblick-Puffer des Konsolenfensters wieder scrollen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;mouse on&amp;amp;#124;off&#039;&#039; || Klick-Auswahl im Kopfbereich. &#039;&#039;&#039;Eingeschaltet&#039;&#039;&#039; ist das Markieren von Text mit der Maus nicht mehr möglich&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;help&#039;&#039; / &#039;&#039;?&#039;&#039; || Zeigt diese Liste in der Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;exit&#039;&#039; / &#039;&#039;quit&#039;&#039; || Beendet den Server geordnet&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Anzeigefilter wirkt &#039;&#039;&#039;nur auf die Anzeige&#039;&#039;&#039;. Was die Konsole&lt;br /&gt;
ausblendet, steht vollständig in RESTSRV_PROTO - es geht nichts verloren.}}&lt;br /&gt;
&lt;br /&gt;
Im Service-Modus (Windows-Dienst) ist die Konsole nicht sichtbar. Alle Einträge landen dort ausschliesslich in der Tabelle RESTSRV_PROTO.&lt;br /&gt;
&lt;br /&gt;
==Weiterführende Seiten==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]] - Schritt-für-Schritt-Anleitung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]] - TLS, mTLS und Bindungen&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]] - API-Keys, JWT, CORS, Host-Beschränkung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Verwaltung, Berechtigungen, WebHooks&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Aufbau der Endpunkt-Skripte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] - Übertragungsprotokoll für Dateien (Multipart, resumable)&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]] - Dateien ausliefern, Range/Resume, öffentliche Artefakte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
</feed>