Facebook
Twitter
Google+
Kommentare
37

Post/Redirect/Get Pattern

Was jetzt wohl kommt? Ein neuer Artikel natürlich. Aber erst mal muss ich mich entschuldigen, dass die Gewinnspielpreise erst in einer Woche raus gehen werden, da ich ab morgen für eine Woche beim Snowboarden bin. Ich hoffe ihr seid ein wenig neidisch. Ich wäre es. Falls ihr jetzt Angst habt, dass ihr sieben Tage nichts über PHP erfahren könnt, dann kann ich euch beruhigen. Es gibt eine Handvoll Co-Autoren, die euch durch die Woche bringen werden.

Soviel erstmal zum Organisatorischen. Jetzt zum Artikel, den ihr bestimmt schon sehnsüchtig erwartet. Es geht um das Post/Redirect/Get Pattern, das einem hilft, mit Post-Methoden umzugehen. Im Speziellen geht es eigentlich darum dass man nicht aus Versehen zwei Mal ein Formular abschickt, wenn man das gar nicht wollte. F5 und die Zurück-Taste im Browser sind da wohl die prominentesten Vertreter für. Ich gehe mal davon aus, dass euch das auch schon öfters passiert ist.

Die Lösung ist eigentlich schon im Namen des Musters beschrieben. Post -> Redirect -> Get. Also der erste Schritt ist ganz normal das Formular per Post abzusenden. Dann verarbeitet ihr in eurer Applikation die Anfrage genau so, wie ihr es immer macht. Bis jetzt sollte euch das Verhalten bekannt vorkommen, denn so werdet ihr eh schon arbeiten. Was man jetzt im „Normalfall“ machen würde ich nach der Verarbeitung die Seite anzuzeigen auf der die Änderungen angezeigt werden.

PostRedirectGet_DoubleSubmitSolution

Das machen wir jetzt aber nicht. Statt die Seite anzuzeigen leiten wir per 301 (permanently moved) weiter auf die Seite. Wir erinnern uns ans Redirect im Namen. Schon haben wir unsere Post-Anfrage in ein einfaches Get umgewandelt. Das tolle daran ist, das der Browser die alte Seite komplett vergessen hat, denn durch den 301 sollte er das auch.

Wenn ich jetzt ein F5 drücke oder im Browser zurückgehe, sollte nicht mehr abgesendet werden. Die Weiterleitung könnt ihr übrigens über die HTTP Header:

<?php
  header("HTTP/1.1 301 Moved Permanently");
  header("Location: http://www.neue-domain.de/bla.html");
  header("Connection: close");
?>

Ich werde das die nächsten Tage für die Ideenschmiede umbauen, denn da kann man nämlich durch F5 ganz einfach bescheißen und das wollen wir ja nicht.

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

37 Comments

  1. Was viele sicherlich von diesem Verfahren abhält ist, dass sie das Problem mit den Erfolgs- bzw. Fehlermeldungen haben, denn ein einfaches Ausgeben der Meldungen geht ja, durch den Redirect, nicht mehr. Den Status über einen GET-Parameter weiterzugeben ist auch keine sehr elegante Lösung für dieses Problem.

    Reply
  2. Als erstes würde ich einfach mal die etwas ketzerische Frage in den Raum werfen: Handelt es sich dabei wirklich um ein Pattern? Gibt es noch andere Problemfelder, wo man das Post/Redirect/Get Pattern einsetzen kann? Oder kann man damit wirklich nur das doppelte absenden von Formulardaten verhindern? Weil in diesem Fall ist es eine Problemlösung, aber noch kein Pattern, weil dafür müsste es auch auf andere Problemfelder anwendbar sein.

    So nun aber zum Beitrag an sich. Wir haben dieses Problem auch gehabt und auch diese Lösung eingesetzt. Was man dabei jedoch beachten muss, ist, dass man manchmal auch Aktionen über GET anstößt. Dann müssen vor dem Redirect nämlich die relevanten GET Parameter aus der URL entfernt werden, befor der Redirekt angestossen wird. Mit einem explode(‚&‘, …), dem auswerten und einem späteren implode(‚&‘, …) geht das auch recht fix.

    Eine andere Lösung, welche denkbar ist wäre dem Formular einen Hash zu geben und wenn das Formular verarbeitet wurde diesen Hash zu invalidieren. Der zweite identische Request wird dann einfach gar nicht verarbeitet. Dies könnte man umsetzen, indem man einen Pool anlegt indem man einen gültigen Hash einfügt und diesen bei Benutzung entfernt. Dabei muss dieser Pool aber je Nutzer angelegt werden, sonst könnte ein Nutzer einen Hash eines anderen Nutzers benutzen.

    Reply
  3. Ich habe es bisher immer folgendermaßen gehandhabt:

    1. POST verarbeiten
    2. unset($_POST);
    3. Redirect auf Erfolgsseite via header(‚Location …‘);

    Hat auch soweit ganz gut funktioniert. Muss aber gestehen, die hier beschriebene Möglichkeit klingt besser. Das werde ich bei Gelegenheit auf jeden Fall mal ausprobieren.

    Reply
  4. @Sven: Ich hoffe ihr stoßt keine Manipulationsanfragen über GET an, denn dafür ist GET nicht gedacht. Dass man aber zwischendurch, je nach Anwendungsfall, Daten bei einem Redirect über GET weitergeben muss, ist ansich nichts ungewöhnliches. Jeder, der darauf reinfällt, dass die Daten nicht beim Redirect mit übergeben werden, wird das schnell merken und die Parameter beim GET mit übergeben.

    Reply
  5. @Markus: Bringt unset($_POST) wirklich etwas? Das $_POST wird ja ansich nur in deiner Anwendung verwendet. Was der Browser macht, ist ansich was ganz anderes, daher sollte den das unsetten von $_POST völlig kalt lassen.

    Reply
  6. Nicht schlecht. Das Problem mit dem drücken der „F5“ Taste habe ich bisher auch öfters gehabt.

    Die Idee ist super, weiß jedoch noch nicht wie ich es umsetzen soll, da man nach meines Wissens vor dem Header keine Ausgabe haben darf. Sehe ich das richtig?

    Reply
  7. Zitat:
    ——————————————
    Was viele sicherlich von diesem Verfahren abhält ist, dass sie das Problem mit den Erfolgs- bzw. Fehlermeldungen haben, denn ein einfaches Ausgeben der Meldungen geht ja, durch den Redirect, nicht mehr. Den Status über einen GET-Parameter weiterzugeben ist auch keine sehr elegante Lösung für dieses Problem.
    ——————————————

    Das sehe ich jetzt ein wenig anders. Es gibt nun einmal derzeit nur diese Methode um ein Formular nicht dopelt abzusenden. Daher muss man darauf zugreifen.

    Reply
  8. Zitat:
    ——————————————
    Die Idee ist super, weiß jedoch noch nicht wie ich es umsetzen soll, da man nach meines Wissens vor dem Header keine Ausgabe haben darf. Sehe ich das richtig?
    ——————————————

    Jop, das ist richtig.

    Reply
  9. Netter Ansatz. Eine andere Möglichkeit ist die Invalidierung durch das Setzen eines Session-Parameters.

    @Jens: Eine Ausgabe sollte eigentlich immer erst erfolgen, wenn der Feature-Layer mit seiner Arbeit durch ist.
    Leider greift in vielen Applikationen, schätzungsweise auch bei dir, schlussfolgernd aus dem Kommentar, das EVA-Prinzip nicht vollständig.

    Die Ausgabe ist die letzte Aktion und sollte sich auf den Datenstrom berufen, der durch Eingabe (natürlich gefiltert) und Feature-Layer erzeugt wird.

    Reply
  10. Zitat:
    ——————————————
    Die Idee ist super, weiß jedoch noch nicht wie ich es umsetzen soll, da man nach meines Wissens vor dem Header keine Ausgabe haben darf. Sehe ich das richtig?
    ——————————————

    Naja entweder man hat schon eine entsprechende Architektur gebaut, dass Actions vor dem Layout etc verarbeitet werden oder man verwendet ob_start(). Dann kann man ganz normal arbeiten und trotzdem jederzeit einen header() abfeuern. Weil der wird nämlich trotzdem rausgeschickt, auch wenn der Output gepuffert wird.

    Reply
  11. Zitat:
    ————————–
    Naja entweder man hat schon eine entsprechende Architektur gebaut, dass Actions vor dem Layout etc verarbeitet werden oder man verwendet ob_start(). Dann kann man ganz normal arbeiten und trotzdem jederzeit einen header() abfeuern. Weil der wird nämlich trotzdem rausgeschickt, auch wenn der Output gepuffert wird.
    ————————–

    Dankesehr! Das hilft mir weiter!

    Reply
  12. Die sinnvollere Methode Form-resubmits zu verhindern sind einfach One-Time-Tokens, die man im Formular einbindet (und in der Session zur Validierung vorhaelt). Das erschwert zusaetzlich noch CSRF, und sollte daher eh jeder machen. Danach muss man keinen HTTP-Header mehr schicken, kann das durch einen Applikations-internen Redirect loesen, und das funktioniert auch fuer Interaktion ueber andere Protokolle als HTTP.

    Reply
  13. Sorry das ich hier ein Doppelpost mache.

    Hat jeder sich schonmal eine gute Architektur gebaut, die mir jemand zur Verfügung stellen könnte. Bin ja noch am lernen 🙁

    Zitat von php.net
    ————————-
    Während die Ausgabepufferung aktiv ist werden Scriptausgaben (mit Ausnahme von Headerinformationen)…

    Mit Ausnahme von Headerinformationen?!
    ————————-

    Reply
  14. Ich muss mich Sven anschließen, ich wusste gar nicht das das ein Pattern ist. Je öfters ich PhpHatesMe lese desto mehr Patterns benutze ich, cool oder ?

    Wie Sven schon geschrieben hat, bevorzuge ich folgende Lösung.

    1. Get Request auf Formular –> Magic Token wird auf Serverseite generiert und ins Formular geschrieben.

    2. Post Request auf Formular –> Prüfe Magic Token ( wurde von benutzer erzeugt und ist noch gültig )

    Beim verarbeiten des Formulares kann ich nun den doppelten Post verhindern. Ganz konrekt verhindere ich das mehrfache versenden von SMS ^^ Also ein sinnvoller Einsatz.

    fertig 😀

    Reply
  15. Meiner Meinung nach keine akzeptable Lösung. Beispielsweise möchte ich das User-Verhalten auf meiner Seite analysieren und erzwinge durch diese Methode einen neuen „Aufruf“. Das bedeutet, dass ich das in meiner Analyse beachten (filtern) muss. Wieso also nicht von vornherein bei den Formularen diesen Fall beachten? Ich prüfe einfach mit entsprechenden Zeit- und Request-Werten. Gleiche Werte dürfen die POST-Werte nur nach einem bestimmten abgelaufenen Zeitfenster tragen, ebenso könnte ich verlangen aktuelle Zeitstempel über ein Hidden-Feld zu übertragen. Dieses lässt ja erkennen, dass ein Refresh getätigt wurde, denn der Wert ist beim Refresh der selbe geblieben (Request wird nochmals gesendet). Ein neuer / veränderter Request dagegen kann ja erwünscht sein. Es gibt noch zahlreiche andere Möglichkeiten, Weiterleitungen finde ich in dem Zusammenhang nicht passend und viel zu umständlich gelöst.

    Grüße

    Reply
  16. @Arvid Bergelmir: Speichere die Nachrichten für die Folgeseite einfach in der Session. Dann kann Dein Script problemlos darauf zugreifen und der Status muss nicht mit im GET übertragen werden.

    Ich überlege gerade, ob man den Redirect auch im Fehlerfall machen sollte. Eigentlich stört dann ein reload nicht, es würde nur denselben Fehler generieren…

    Reply
  17. @Balu: Guter Punkt. So mache ich es auch meistens: Wenn die Formularverarbeitung erfolgreich war, dann Aktion auslösen und Redirect. War sie erfolglos, kann ich mir das Speichern der Validierungsfehler in der Session sparen, indem ich keinen Redirect mache.

    Reply
  18. Ich mache es so, dass ich die Daten validiere, die Aktionen ausführe und bei Erfolg per Redirect auf die nächste Seite weiterleite. Erfolgsmeldungen werden vorrübergehend bis zum Abruf in der Session zwischengespeichert. Wurde die Meldung angezeigt, verschwindet die Meldung aus der Session. Im Fehlerfall wird nicht weitergeleitet, sondern intern wieder auf die Herkunftsseite umgeroutet, damit diese wieder angezeigt werden kann. Die Fehlermeldungen werden nicht in der Session zwischengespeichert, da auch kein Redirect stattfindet. Der Benutzer befindet sich also noch auf einer POST-Seite. Bei Druck auf F5 wird der Benutzer also abgefragt, ob er die Daten nochmals senden möchte.

    Reply
  19. Da ist man mal ’nen Vormittag im Meetingmarathon und schon muss man 23 Kommentare lesen … sehr coole Sache! Der Ausdruck Design Pattern also Entwurfsmuster habe ich aus der Wikipedia entnommen (http://en.wikipedia.org/wiki/Post/Redirect/Get) wo übrigens auch die Grafik her ist. Ob der Ausdruck nun komplett richtig verwendet wurde will ich hier gar nicht entscheiden.

    Die Token-Geschichte finde ich auch sehr sympatisch, dann aber in Kombination mit der Redirect-Variante, denn so Kombiniere ich Sicherheit und Bequemlichkeit und ich denke mehr kann man kaum erreichen.

    Falls ich nämlich nur das Token verwende, wird mich der Browser immer noch fragen, ob er die Daten erneut senden soll und das nervt mich an manchen Tagen schon ein wenig.

    Reply
  20. Achja, Tokens verwende ich natürlich auch 🙂 Habe einen Token-Stack von 10 Tokens pro Benutzer. So kann der Benutzer auch in unterschiedlichen Tabs arbeiten ohne, dass gleich der Token für ein Formular abläuft.

    Reply
  21. Hi,
    um das mit dem Header in den Griff zu bekommen, könnte man eine Klasse erstellen welche die ganz Ausgabe abwickelt. Diese könnte z.B. HttpResponse heißen. Die Klasse speichert dann alles in einer Eigenschaft, und sendet erst am Ende des Skripts (evtl. im Destruktor…) das Dokument an den Browser.

    Ansonsten ich vielleicht noch hinzufügen, dass man den Besucher darauf hinweisen sollte, dass er das Formular nur einmal abschicken muss/darf.

    Liebe Grüße Paloran und einen schönen Urlaub (und ob ich neidisch bin…)

    P.s. wenn man nur abends Blogs liest, sind da immer schon unzählige Kommentare…

    Reply
  22. Hallo,
    hat das Schließen der Verbindung mit header(„Connection: close“); einen Sinn? Gerade wenn man dem Browser mitteilt, dass er sich nach einem POST per GET an eine andere URL wenden soll, wäre es doch geschickter, dass der Browser die TCP Verbindung offen hält?

    Reply
  23. Ich mache es prinzipiell so wie Arvid, dass ich bei einem Fehlerfall auf dem Formular bleibe und im Erfolgsfall einen Redirect durchführe. Wer das Zend Framework verwendet kann zum Beispiel den Flash Messenger verwenden, so hat man ein Systemweites „Meldesystem“ – oder hat man hat seine eigene Implementierung, die ein paar mehr Fälle abdecken kann 😉

    Reply
  24. Kann es sein dass man in der Ideenschmiede immer noch mehrmals dasselbe hintereinander wählen kann; oder funktioniert dieses Pattern nicht?

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen