Verschiedene Aspekte einer idempotent API verstehen?

PFEdi

Mitglied
Hallo,

ich setzte mich gerade mit dem Thema „API idempotence“ auseinander, und wollte euch zu meinem besseren Verständnis eigene Fragen stellen ob ich es richtig verstehe:

1) „idempotence“ ist vor allem eine Technische Betrachtung der API und weniger Domain spezifisch
Damit meine ich das damit v.a. auf technischer ebene adressiert wird, dass ein Request zum System den Zustand (des Systems/der Daten) nicht irrtümlich ändert. Z.b. Wenn die Response verloren geht, kann der Nutzer/Client nochmals den genau gleichen Request senden, und sich bei einer idempotenten API sicher sein, das nicht versehentlich ein unerwünschte Ereignisse auslöst wird (e.g.: 2 mal dasselbe Produkt bestellen/bezahlen).
Stimmt diese Sichtweise? Oder gibt es auch eine Domänen spezifische Aspekt? Klar schützt man die Domäne mit, aber auf einem Technischen level. mMn sollte die Domänen Seite eher ducrh Business Rules adressiert, verhindert werden.


2) Die Technische Implementierung von idempotence ist nicht leicht 😉
So wie ich es versteh, muss das vom Client „mit Unterstützt“ werden, in dem beim Request ein „einzigartiger Schlüssel“ (e.g.: UUID) mit gesendet wird. Dieser kann dann vom Server genutzt werden um zu vergleichen ob er diesen Request schon mal bekommen hat.
Nein -> er verarbeitet die Anfrage, und fügt in eine Tabelle den „einzigartiger Schlüssel“ (e.g.: UUID) + die Response ein, und sendet die Reponse zurück.
Ja -> er kann mit der abfrage der Tabelle die bereits bekannte Response zurück schicken.
Frage: Gibt es auch andere Implementierungs-Konzepte?
(Den Ganzen Request speichern könnte ja sehr in den Speicher gehen, und müsste ggf Time Stamps, Signaturen etc irgendwie berücksichtigen)

2.1) idempotence wird nie unendlich gewährt.
Der Client schickt mit dem „einzigartiger Schlüssel“ (e.g.: UUID) auch eine Validity Time (z.B.: 1 Tag) + time stamp für den dieser Request gültig ist.
Der Server Speichert sich diesen wert + wann der Request verarbeitet wurde. Damit er alle Einträge aus seiner Tabelle löschen kann, die älter als Request Time + Validity Time Range sind.
(Alternative kann man auch sagen, dass der Server selbst einen Default hat, wie lange er die Einträge als gültig erachtet)
Frage: Stimmt das so?
Gibt es noch andere Konzepte die idempotence zeitlich zu limitieren? (oder macht man das irgendwo nicht?)
Wie bestimmt man die Zeit wie lange man idempotence gewähren will?

Was sagt ihr dazu?
 
„API idempotence“ bedeutet nicht, dass immer das genau gleiche Ergebnis aus einer API zurückgegeben wird.
Idempotenz bedeutet im Wesentlichen, dass die Auswirkung einer erfolgreich durchgeführten Anfrage auf eine Serverressource unabhängig davon ist, wie oft sie ausgeführt wird.

Wenn N ähnliche DELETE-Anfragen ausgeführt, löscht die erste Anfrage die Ressource und die Antwort lautet 200 (OK) oder 204 (kein Inhalt).
Die weiteren N-1 Anfragen geben 404 (Not Found) zurück. (Wobei ich hier in der Praxis keinen Unterschied mache. Ich gebe immer 204 oder 404 zurück - je nach Projekt, wenn die Ressource nicht existiert.)
Natürlich unterscheidet sich dann die Antwort von der ersten Anfrage, aber es gibt keine Zustandsänderung für eine Ressource auf der Serverseite, da die ursprüngliche Ressource bereits gelöscht ist.
 
Bei Delete macht ein 404 noch Sinn. Was gibst du denn bei einem Create durch eine Post Request zurück, welcher schon angelegt wird?

Ich frage deswegen, weil ich selber für das Szenario 304 zurückgebe(Not modified) und mich gerade selber Frage, ob das irgend so OK ist.

Tatsächlich lösche ich grundsätzlich nichts aus der Datenbank und deaktivieren mit einer Flag höchsten Datensätze. Aber das ist natürlich ein ganz anderes Thema.
 
Ich frage deswegen, weil ich selber für das Szenario 304 zurückgebe(Not modified) und mich gerade selber Frage, ob das irgend so OK ist.
304 ist die Antwort auf einen lesenden Request. Der Server sagt damit: hey, Du hast das Zeug schon, ich brauche und werde es nicht nochmal schicken.

Wir verwenden wie @Nouser für den angesprochenen Fall 409 Conflict, da die Semantik dem Fall wohl am nächsten kommt: https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8
 
Was gibst du denn bei einem Create durch eine Post Request zurück, welcher schon angelegt wird?
Tatsächlich ist POST ist NICHT idempotent. Wenn man also die Anfrage N-mal wiederholt, werden am Ende N Ressourcen mit N verschiedenen URI's auf dem Server erstellt. Das kann ja auch nicht anders sein, denn Post-Requests legen eine neue Ressource an und besitzen noch keine ID welche in einer URI verwendet werden könnten.
1725984638057.png
 
Hey, danke für die Antworten. Das Hilft mir beim Verständnis - allerdings
Wird hier HTTP verallgemeinert/vereinfacht (sorry mir fällt kein besserer Begriff dafür ein) - weil wie implementiere ich das denn?
Woher kommen die Implementierungen, so wie ich sie in Punkt 2 Beschrieben habe? (e.g.: Stripe)
Stammen solche Implementierungen aus zu langer Vergangenheit?
(das scheint ja mal so unabhängig von HTTP zu sein).

Weil wenn ich Betrag X von Konto A nach Konto B überweißen will -> mache ich das mit welcher HTTP methode? (PUT weil änderung?)
Aber wie sieht die idempotency dabei aus?
Natürlich kann ich den Request 100 schicken und das System soll sich 100 ändern (wenn das Konto es zu lässt).
Dennoch macht es ja keinen Sinn - wenn es nur wegen einem "verlorenen" ACK nochmal gesendet wurde .
 
Tatsächlich ist POST ist NICHT idempotent. Wenn man also die Anfrage N-mal wiederholt, werden am Ende N Ressourcen mit N verschiedenen URI's auf dem Server erstellt. Das kann ja auch nicht anders sein, denn Post-Requests legen eine neue Ressource an und besitzen noch keine ID welche in einer URI verwendet werden könnten.
In der Theorie ist das korrekt. In der Praxis habe ich das ganz oft anders gesehen. Da wird ein Post an den Server gesendet, dann wird eine neue Ressource erstellt und einer der Parameter im Request belegt dann ein DB Feld, welches unique ist. Bedeutet, ein erneuter POST Request mit den selben Parameter kann nicht nochmal in der DB angelegt werden.
 
Wird hier HTTP verallgemeinert/vereinfacht (sorry mir fällt kein besserer Begriff dafür ein) - weil wie implementiere ich das denn?
Woher kommen die Implementierungen, so wie ich sie in Punkt 2 Beschrieben habe? (e.g.: Stripe)
Stammen solche Implementierungen aus zu langer Vergangenheit?
(das scheint ja mal so unabhängig von HTTP zu sein).

Weil wenn ich Betrag X von Konto A nach Konto B überweißen will -> mache ich das mit welcher HTTP methode? (PUT weil änderung?)
Aber wie sieht die idempotency dabei aus?
Natürlich kann ich den Request 100 schicken und das System soll sich 100 ändern (wenn das Konto es zu lässt).
Dennoch macht es ja keinen Sinn - wenn es nur wegen einem "verlorenen" ACK nochmal gesendet wurde .
Die Fragen sind fast zu generisch, als das man wirklich gut drauf antworten könnte.

Generell musst du nur irgendwie dafür sorgen, dass ein wiederholtes Ausführen einer Aktion keine anderen Auswirkungen wie ein einmaliges Ausführen einer Aktion hat - was du dabei (auf nicht-HTTP-Ebene) als eine Aktion und als Auswirkungen ansiehst und wie du das ganze sicherstellst bleibt da völlig dir überlassen. Die Eine Implementierung gibt es da nicht wirklich - eher übliche Pattern, wie z.B. bei Zahlungsanbietern wie z.B. Stripe.

Sehr grob und vereinfacht initiierst du in einem Request eine Transaktion (= Zahlungsvorgang) und in einem weiterem Request führst du den dann durch.
Das initiieren ist dabei fachlich idempotent (technisch nicht unbedingt, da die Transaktion natürlich irgendwie gespeichert wird), es ändert sich kein relevanter Zustand und du kannst das ganze theoretisch beliebig oft durchführen - es wird einfach jeder mal eine neue Transaktion initiiert, aber solange du damit nichts weiter machst ist das egal. Das Durchführen der Transaktion ist dann auch fachlich idempotent, nur solange die Transaktion nicht bereits durchgeführt wurde, wird noch Geld gebucht - wenn das für diese Transaktion schon gemacht wurde, wird einfach nichts mehr gemacht. (Wichtig ist: beide Teilschritte für sich sind idempotent, der Gesamtprozess aber nicht.)

In der Theorie ist das korrekt. In der Praxis habe ich das ganz oft anders gesehen. Da wird ein Post an den Server gesendet, dann wird eine neue Ressource erstellt und einer der Parameter im Request belegt dann ein DB Feld, welches unique ist. Bedeutet, ein erneuter POST Request mit den selben Parameter kann nicht nochmal in der DB angelegt werden.
Der Punkt auf den @Oneixee5 wohl hinauswollte: POST ist nicht als Idempotent spezifiziert, anders als zB DELETE.

Das einzelne APIs auch bei POST idempotent sind ist da kein Widerspruch und im Rahmen der Spezifikation (POST darf relativ explizit auch idempotent sein). Bei Delete z.B. ist das andersrum: ein nicht idempotentes DELETE widerspricht der Spezifikation. Bei DELETE sollte man sich rein auf HTTP-Ebene darauf verlassen können, bei POST eben nicht.
 
ich setzte mich gerade mit dem Thema „API idempotence“ auseinander, und wollte euch zu meinem besseren Verständnis eigene Fragen stellen ob ich es richtig verstehe:

1) „idempotence“ ist vor allem eine Technische Betrachtung der API und weniger Domain spezifisch
2) Die Technische Implementierung von idempotence ist nicht leicht 😉
2.1) idempotence wird nie unendlich gewährt.
Um darauf noch mal explizit kurz und knapp zu antworten:

1: Falsch.
2: Falsch.
2.1: Falsch.

🙂
 
2: Sehe ich nicht so

Wiederlegt man am einfachsten mit einem Gegenbeispiel:
Deine Aussage war "Die Technische Implementierung von idempotence ist nicht leicht" - der simpelste Fall für eine idempotente API ist eine API ganz ohne Seiteneffekte, und da ist der simpelste Fall eine API die genau nichts macht, fällt dir noch was leichteres ein?
Oder wenn du ein relitätsnäheres Beispiel haben möchtest: Löschen einer Entität anhand ihrer ID, kann man beliebig oft wiederholen, die Entität ist nach dem ersten Mal gelöscht und bleibt es auch bei den weiteren Malen, auch ohne das man irgendwas beachtet.

Deine ganze Erläuterung, warum das nicht leicht ist, ist dagegen ein sehr spezifischer Fall der extra kompliziert ist und alle Teile davon sind nicht nötig sondern tatsächlch sogar eher unüblich (bis auf den "Client muss es unterstützen"-Fall, aber das ist in den meisten Fällen einfach schon fachlich durch den Anwendungsfall selbst vorgegeben und nicht extra für Idempotenz zusätzlich implementiert)
 
Ich sehe auch nicht - außer in Edge Cases - wo Idempotence schwierig ist. Es gibt aus meiner Sicht zwei Arten:

API ist von Haus aus bedingt durch ihre Implementierung idempotent (Lesen, Delete etc.)
Das ist nicht kompliziert, sondern ergibt sich natürlich in der Implementierung.

API ist nicht idempotent und soll idempotent gemacht werden
Hier stellt sich die erste Frage - warum? Während im obigen Fall es meist eine fachliche Notwendigkeit/Automatismus ist, ist hier nicht. Beispielsweise die API um Geld auf ein Konto einzuzahlen ist per se nicht idempotent. Wenn ich 2x 200 EUR einzahle erwarte ich ja auch, dass ich 400 EUR eingezahlt habe.
Hier ist der Zweck der Idempotence in der Regel Resilence gegen Netzwerkprobleme etc - es geht darum sicherzustellen, dass ein Request mindestens 1x verarbeitet wird, auch bei Netzwerkproblemen etc.
Und dann kommt oft die erwähnte UUID ins Spiel - aber dann komplett auf technischer Ebene. Der Client erzeugt eine UUID und packt sie an den Request. Und sendet den Request so oft an den Server ab, bis eine Antwort bekommen habt, dass er angekommen ist. Der Server nimmt die Requests entgegen und stellt anhand der UUID sicher, dass nix doppelt verarbeitet wird. Das ist dann aber oft eine Art Proxy um die API drum hat nichts mehr mit der eigentlichen Implementierung der API zu tun. Das ist auch nicht besonders komplex.


Komplex wird es dann, wenn ich eine fachlich nicht idempotente API auf fachlicher Ebene idempotent machen will - aber da stellt sich dann die Sinnfrage. Warum will man das? Entweder sie ist fachlich idempotent - dann muss ich nichts tun. Oder sie ist es nicht - dann kann ich Idempotence als technischen Layer drumherum bauen.
 
Und dann kommt oft die erwähnte UUID ins Spiel - aber dann komplett auf technischer Ebene. Der Client erzeugt eine UUID und packt sie an den Request. Und sendet den Request so oft an den Server ab, bis eine Antwort bekommen habt, dass er angekommen ist. Der Server nimmt die Requests entgegen und stellt anhand der UUID sicher, dass nix doppelt verarbeitet wird.
Wie behandelt man bei einer UUID eigentlich den Fall, wenn zwar der Request verarbeitet wurde aber die Response(2xx) den Client nie erreicht hat und dann durch ein 500 quittiert wurde ? Weil zwischendurch die Verbindung abgebrochen ist?
 
Wenn der 2xx Request den Client nie erreicht, bekommt er auch kein 500er - sondern "Connection Timeout".
Dann sendet der Client den Request nochmal (mit der gleichen UUID) und der Server antwortet dann mit "Schon verarbeitet". In welcher Form auch immer, dass ist zu definieren, welcher Resposne Code gesendet wird.
 

Zurück
Oben