Wie soll eine REST-API PUT-Anforderungen an teilweise veränderbare Ressourcen verarbeiten?

46

Angenommen, eine REST-API gibt als Antwort auf eine HTTP- GETAnforderung einige zusätzliche Daten in einem Unterobjekt zurück owner:

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'Programmer'
  }
}

Natürlich wollen wir nicht, dass jemand PUTzurück kann

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'CEO'
  }
}

und haben das geschafft. Tatsächlich sind wir wahrscheinlich nicht gehen sogar implementieren einen Weg , dass sogar potenziell erfolgreich zu sein, in diesem Fall.

Bei dieser Frage geht es jedoch nicht nur um Unterobjekte: Was sollte im Allgemeinen mit Daten geschehen, die in einer PUT-Anforderung nicht geändert werden dürfen?

Sollte es erforderlich sein, in der PUT-Anforderung zu fehlen?

Sollte es stillschweigend weggeworfen werden?

Sollte es überprüft werden und wenn es vom alten Wert dieses Attributs abweicht, einen HTTP-Fehlercode in der Antwort zurückgeben?

Oder sollten wir RFC 6902-JSON-Patches verwenden, anstatt die gesamte JSON zu senden?

Robin Green
quelle
2
All dies würde funktionieren. Ich denke, es hängt von Ihren Anforderungen ab.
Robert Harvey
Ich würde sagen, dass der Grundsatz der geringsten Überraschung darauf hindeutet, dass er in der PUT-Anfrage fehlen sollte. Ist dies nicht möglich, überprüfen Sie, ob es sich um ein anderes handelt, und geben Sie den Fehlercode zurück. Stilles Verwerfen ist am schlimmsten (Benutzer erwartet, dass sich dies ändern wird, und Sie sagen ihnen "200 OK").
Maciej Piechotka
2
@MaciejPiechotka das Problem dabei ist, dass Sie nicht das gleiche Modell auf dem Put verwenden können wie auf dem Insert oder Get etc, ich würde es vorziehen, dass das gleiche Modell verwendet wird und es einfach Feldautorisierungsregeln gibt, wenn sie einen Wert für eingeben In einem Feld, das sie nicht ändern sollten, erhalten sie einen 403 Forbidden zurück, und wenn später eine Autorisierung eingerichtet wird, um dies zuzulassen, erhalten sie einen 401 Unauthorized, wenn sie nicht autorisiert sind
Jimmy Hoffa
@JimmyHoffa: Mit Modell ist das Datenformat gemeint (da es je nach Auswahl möglich sein könnte, das Modell im MVC Rest-Framework wiederzuverwenden, falls eines verwendet wird - OP hat keines erwähnt). Ich würde mich für die Auffindbarkeit entscheiden, wenn ich nicht an das Framework gebunden wäre und ein früher Fehler etwas auffindbarer / einfacher zu implementieren wäre, als nach Änderungen zu suchen (ok - ich sollte das Feld XYZ nicht berühren). In jedem Fall ist das Wegwerfen am schlimmsten.
Maciej Piechotka

Antworten:

46

Es gibt weder in der W3C-Spezifikation noch in den inoffiziellen Regeln von REST eine Regel, die besagt, dass a PUTdasselbe Schema / Modell wie sein entsprechendes verwenden muss GET.

Es ist schön, wenn sie ähnlich sind , aber es ist nicht ungewöhnlich PUT, Dinge etwas anders zu machen. Ich habe zum Beispiel viele APIs gesehen, die der Einfachheit halber eine Art ID in den von a zurückgegebenen Inhalten enthalten GET. Aber mit a PUTwird diese ID ausschließlich von der URI bestimmt und hat im Inhalt keine Bedeutung. Jede im Body gefundene ID wird unbemerkt ignoriert.

REST und das Web im Allgemeinen sind stark an das Robustness-Prinzip gebunden : "Sei konservativ in dem, was du tust [send], sei liberal in dem, was du akzeptierst." Wenn Sie dem philosophisch zustimmen, liegt die Lösung auf der Hand: Ignorieren Sie ungültige Daten in PUTAnfragen. Dies gilt sowohl für unveränderliche Daten wie in Ihrem Beispiel als auch für tatsächlichen Unsinn, z. B. unbekannte Felder.

PATCHDies ist möglicherweise eine weitere Option, die Sie jedoch nur implementieren sollten, PATCHwenn Sie auch Teilaktualisierungen unterstützen. PATCHbedeutet, nur die spezifischen Attribute zu aktualisieren, die ich in den Inhalt einbeziehe ; Dies bedeutet nicht , dass die gesamte Entität ersetzt wird, sondern dass einige bestimmte Felder ausgeschlossen werden . Worüber Sie tatsächlich sprechen, ist kein teilweises Update, sondern ein vollständiges Update, idempotent und alles, nur ein Teil der Ressource ist schreibgeschützt.

Wenn Sie diese Option auswählen, können Sie eine 200 (OK) mit der tatsächlich aktualisierten Entität in der Antwort zurücksenden, damit die Clients klar erkennen, dass die schreibgeschützten Felder nicht aktualisiert wurden.

Es gibt sicherlich einige Leute , die anders denken - dass es ein Fehler sein sollte, zu versuchen, einen schreibgeschützten Teil einer Ressource zu aktualisieren. Es gibt eine Rechtfertigung dafür, hauptsächlich auf der Grundlage, dass Sie definitiv einen Fehler zurückgeben würden, wenn die gesamte Ressource schreibgeschützt wäre und der Benutzer versucht hätte, sie zu aktualisieren. Es verstößt definitiv gegen das Robustheitsprinzip, aber Sie könnten es als "selbstdokumentierend" für Benutzer Ihrer API ansehen.

Hierfür gibt es zwei Konventionen, die beide Ihren ursprünglichen Vorstellungen entsprechen, aber ich werde sie erweitern. Die erste besteht darin, zu verhindern, dass schreibgeschützte Felder im Inhalt angezeigt werden, und in diesem Fall HTTP 400 (Bad Request) zurückzugeben. APIs dieser Art sollten auch ein HTTP 400 zurückgeben, wenn andere nicht erkannte / nicht verwendbare Felder vorhanden sind. Die zweite besteht darin, dass die schreibgeschützten Felder mit dem aktuellen Inhalt identisch sein müssen und eine 409 (Konflikt) zurückgeben, wenn die Werte nicht übereinstimmen.

Ich mag die Gleichheitsprüfung mit 409 wirklich nicht, weil der Client immer eine GETausführen muss, um die aktuellen Daten abzurufen, bevor er eine ausführen kann PUT. Das ist einfach nicht schön und wird wahrscheinlich zu einer schlechten Leistung führen, für jemanden, irgendwo. Ich mag 403 (Verboten) auch wirklich nicht, da dies impliziert, dass die gesamte Ressource geschützt ist, nicht nur ein Teil davon. Wenn Sie also unbedingt validieren müssen, anstatt dem Robustheitsprinzip zu folgen, validieren Sie alle Ihre Anforderungen und geben Sie eine 400 für alle mit zusätzlichen oder nicht beschreibbaren Feldern zurück.

Vergewissern Sie sich, dass Ihre 400/409 / whatever Informationen über das jeweilige Problem und dessen Behebung enthält.

Beide Ansätze sind gültig, aber ich bevorzuge den ersteren, um dem Robustheitsprinzip zu entsprechen. Wenn Sie schon einmal mit einer großen REST-API gearbeitet haben, werden Sie den Wert der Abwärtskompatibilität zu schätzen wissen. Wenn Sie jemals ein vorhandenes Feld entfernen oder schreibgeschützt machen möchten, handelt es sich um eine abwärtskompatible Änderung, wenn der Server diese Felder einfach ignoriert und alte Clients weiterhin funktionieren. Wenn Sie den Inhalt jedoch streng überprüfen, ist er nicht mehr abwärtskompatibel und alte Clients funktionieren nicht mehr. Ersteres bedeutet im Allgemeinen weniger Arbeit für den Betreuer einer API und seine Clients.

Aaronaught
quelle
1
Gute Antwort und positiv bewertet. Ich bin mir jedoch nicht sicher, ob ich damit einverstanden bin: "Wenn Sie jemals beschließen, ein vorhandenes Feld zu entfernen oder schreibgeschützt zu machen, handelt es sich um eine abwärtskompatible Änderung, wenn der Server diese Felder einfach ignoriert und alte Clients weiterhin funktionieren. " Wenn sich der Client auf dieses entfernte / neu schreibgeschützte Feld verlassen würde, hätte dies dann noch keinen Einfluss auf das Gesamtverhalten der App? Im Falle des Entfernens von Feldern würde ich argumentieren, dass es wahrscheinlich besser ist, explizit einen Fehler zu generieren, anstatt die Daten zu ignorieren. Andernfalls hat der Client keine Ahnung, dass das zuvor funktionierende Update jetzt fehlschlägt.
Rinogo
Diese Antwort ist falsch. Aus zwei Gründen von RFC2616: 1. (Abschnitt 9.1.2) PUT muss unabhängig sein. Wenn Sie viele Male setzen, wird das gleiche Ergebnis wie beim einmaligen Setzen erzielt. 2. Der
Befehl
1
Was ist, wenn Sie die Gleichheitsprüfung nur dann durchführen, wenn der unveränderliche Wert in der Anforderung gesendet wurde? Ich denke, das gibt dir das Beste aus zwei Welten; Sie erzwingen nicht, dass Clients ein GET ausführen, und Sie benachrichtigen sie trotzdem, dass etwas nicht stimmt, wenn sie einen ungültigen Wert für eine unveränderliche Datei gesendet haben.
Ahmad Abdelghany
Danke, der ausführliche Vergleich, den Sie in den letzten Abschnitten aus Erfahrung angestellt haben, ist genau das, wonach ich gesucht habe.
Dhill
9

Idem Potenz

Nach dem RFC müsste ein PUT das gesamte Objekt an die Ressource liefern. Der Hauptgrund dafür ist, dass PUT idempotent sein sollte. Dies bedeutet, dass eine wiederholte Anforderung auf dem Server zu demselben Ergebnis führen sollte.

Wenn Sie Teilaktualisierungen zulassen, kann es nicht mehr idem-potent sein. Wenn Sie zwei Kunden haben. Client A und B, dann kann das folgende Szenario entstehen:

Client A erhält ein Bild von Ressourcenbildern. Dies enthält eine Beschreibung des Bildes, die noch gültig ist. Der Client B erstellt ein neues Image und aktualisiert die Beschreibung entsprechend. Das Bild hat sich geändert. Kunde A sieht, er muss die Beschreibung nicht ändern, weil es so ist, wie er es wünscht und nur das Bild setzt.

Dies führt zu einer Inkonsistenz, das Bild hat die falschen Metadaten angehängt!

Noch ärgerlicher ist, dass jeder Vermittler die Anfrage wiederholen kann. Falls sich herausstellt, dass der PUT fehlgeschlagen ist.

Die Bedeutung von PUT kann nicht geändert werden (obwohl Sie es missbrauchen können).

Andere Optionen

Zum Glück gibt es noch eine andere Option: PATCH. PATCH ist eine Methode, mit der Sie eine Struktur teilweise aktualisieren können. Sie können einfach eine Teilstruktur senden. Für einfache Anwendungen ist dies in Ordnung. Es wird nicht garantiert, dass diese Methode gleich wirksam ist. Der Kunde sollte eine Anfrage in der folgenden Form senden:

PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 20
{fielda: 1, fieldc: 2}

Der Server kann mit 204 (ohne Inhalt) antworten, um den Erfolg zu melden. Im Fehlerfall können Sie einen Teil der Struktur nicht aktualisieren. Die PATCH-Methode ist atomar.

Der Nachteil dieser Methode ist, dass dies nicht von allen Browsern unterstützt wird. Dies ist jedoch die natürlichste Option in einem REST-Service.

Beispiel für eine Patch-Anforderung: http://tools.ietf.org/html/rfc5789#section-2.1

Json Patchen

Die json-Option scheint ziemlich umfassend und eine interessante Option zu sein. Die Implementierung für Dritte kann jedoch schwierig sein. Sie müssen sich entscheiden, ob Ihre Benutzerbasis damit umgehen kann.

Es ist auch etwas kompliziert, da Sie einen kleinen Interpreter erstellen müssen, der die Befehle in eine Teilstruktur konvertiert, die Sie zum Aktualisieren Ihres Modells verwenden werden. Dieser Interpreter sollte auch prüfen, ob die angegebenen Befehle sinnvoll sind. Einige Befehle heben sich gegenseitig auf. (Feld schreiben, Feld löschen). Ich denke, Sie möchten dies dem Client zurückmelden, um die Debug-Zeit auf seiner Seite zu begrenzen.

Aber wenn Sie die Zeit haben, ist dies eine wirklich elegante Lösung. Sie sollten die Felder natürlich trotzdem validieren. Sie können dies mit der PATCH-Methode kombinieren, um im REST-Modell zu bleiben. Aber ich denke POST wäre hier akzeptabel.

Es wird schlecht

Wenn Sie sich für die Option PUT entscheiden, ist dies etwas riskant. Dann sollten Sie den Fehler zumindest nicht verwerfen. Der Benutzer hat eine gewisse Erwartung (die Daten werden aktualisiert) und wenn Sie dies brechen, werden Sie einigen Entwicklern keine gute Zeit geben.

Sie können wählen, ob Sie zurückweisen möchten: 409 Conflict oder 403 Forbidden. Es hängt davon ab, wie Sie den Aktualisierungsprozess betrachten. Wenn Sie es als eine Reihe von Regeln sehen (systemzentriert), wird der Konflikt schöner sein. Diese Felder können nicht aktualisiert werden. (Im Widerspruch zu den Regeln). Wenn Sie es als Autorisierungsproblem ansehen (benutzerzentriert), sollten Sie verboten zurückkehren. Mit: Sie sind nicht berechtigt, diese Felder zu ändern.

Sie sollten Benutzer dennoch zwingen, alle änderbaren Felder zu senden.

Eine sinnvolle Möglichkeit, dies durchzusetzen, besteht darin, sie auf eine Subressource festzulegen, die nur die veränderbaren Daten anbietet.

Persönliche Meinung

Persönlich würde ich mich für das einfache PATCH-Modell entscheiden (wenn Sie nicht mit Browsern arbeiten müssen) und es später mit einem JSON-Patch-Prozessor erweitern. Dies kann durch Unterscheidung der Mimetypen erfolgen: Der Mime-Typ des Json-Patches:

Anwendung / Json-Patch

Und json: application / json-patch

macht es einfach, es in zwei Phasen zu implementieren.

Edgar Klerks
quelle
3
Ihr Beispiel für Idempotenz ergibt keinen Sinn. Entweder ändern Sie die Beschreibung oder Sie tun es nicht. In jedem Fall erhalten Sie jedes Mal das gleiche Ergebnis.
Robert Harvey
1
Sie haben recht, ich denke, es ist Zeit, ins Bett zu gehen. Ich kann es nicht bearbeiten. Es ist eher ein Beispiel für das rationale Senden aller Daten in einer PUT-Anforderung. Danke für den Hinweis.
Edgar Klerks
Ich weiß, dass dies vor 3 Jahren war ... aber wissen Sie, wo ich im RFC weitere Informationen zu "PUT müsste das gesamte Objekt an die Ressource liefern" finden kann? Ich habe dies an anderer Stelle erwähnt gesehen, würde aber gerne sehen, wie es in der Spezifikation definiert ist.
CSharper
Ich glaube, ich habe es gefunden? tools.ietf.org/html/rfc5789#page-3
CSharper