WordPress: Remote HTTP Request vor Timeouts absichern

Zusätzliche Daten & Informationen über einen HTTP Requests in einem WordPress Plugin oder Themen zu laden ist eine sehr schöne Möglichkeit den eigenen Content mit zusätzlichen Informationen anzureichern. Ich nutzte für Travel-Dealz.de mehrere APIs z.B. um Klimadiagramme für Reiseziele bereitzustellen oder Status- und Vielfliegermeilen für einen Flug zu berechnen.

Allerdings werden die Abfragen i.d.R. beim Seitenaufruf durchgeführt und das kann den Seitenaufruf für den Besucher der Seite stark verzögern oder im schlimmsten Fall zu einem Timeout führen = der Besucher erhält nur eine Fehlermeldung und verlässt die Seite wieder.

Nutzt man die Funktionen wp_remote_get bzw. wp_remote_post, sollte man sich einige Gedanken machen wie man Timeouts verhindert, z.B. wenn die angesprochene API nicht erreichbar ist (das wird garantiert irgendwann passieren) und wie man verhindert, dass der Request bei jedem Seitenaufruf erfolgen muss.

Ergebnis zwischenspeichern

Um die Antwortzeit eures Servers trotz externer Requests so kurz wie möglich zu halten, solltet ihr jeden externen Request eigentlich verhindern. Deswegen solltet ihr nicht nutzerspezifische Inhalte immer zwischenspeichern = cachen!

WordPress bietet zwei Möglichkeiten Daten für eine bestimmte Zeit zu speichern. Und genau das wollen wir. Damit die gespeicherten Daten mit der API übereinstimmen, sollten sie nicht dauerhaft, sondern i.d.R. nur für einen bestimmten Zeitraum d.h. eine Minute, Stunde, Tag, Woche, Monat… zwischengespeichert werden. Wie lange genau, müsst ihr selber einschätzen. Wetterdaten vom aktuellen Tag sind sicherlich nicht mehr nach einer Woche relevant. Börsenkurse vielleicht nur Sekunden bis Minuten.

Transient

Transiente werden in der wp_options Tabelle in der Datenbank gespeichert.

Beim Abruf wird immer überprüft, ob die Transiente anhand eines Zeitstempels noch gültig ist. Wenn nicht, wird sie gelöscht und steht nicht mehr zur Verfügung. Der beim Erstellen definierte Zeitraum garantiert euch allerdings nicht, dass die Daten noch bis zu dem Ablaufzeitpunkt verfügbar sind, sondern nur, dass sie nach dem Zeitraum definitiv nicht mehr verfügbar sind.

$transient_key = 'my_api_request_xy'; if ( false === ( $result = get_transient( $transient_key ) ) ) { $response = wp_remote_get( 'https://go-around.de/wp-json/' ); $result = json_decode( $response['body'] ); set_transient( 'my_request_xy_exceeded', $result, HOUR_IN_SECONDS ); } // Do future work with $result
Code-Sprache: PHP (php)

Ihr müsst immer einen Key definieren, unter dem ihr das Ergebnis auch wiederfindet. Fragt ihr mit unterschiedlichen Parametern eine API ab, müsst ihr sicherstellen, dass der Key auch immer unterschiedlich ist. Ich nutze dafür gerne die implode-Funktion, die die Values eines Arrays in einen String umwandeln, z.B. getrennt durch ein _

$request = [ 'id' => 123, 'type' => 'post', ]; $transient_key = 'my_api_request_xy_' . esc_attr( implode( '_', $request ) );
Code-Sprache: PHP (php)

Die esc_attr Funktion sichert den erstellten Key nur noch einmal ab. Eigentlich ist sie für die Verwendung in HTML Attributen gedacht und entfernt z.B. ein ".

Bedenkt aber, Transiente werden in der Datenbank gespeichert und obwohl sie in der wp_options Tabelle gespeichert sind, verursacht jeder Aufruf von get_transient eine Abfrage an die Datenbank. Auch das kann Zeit kosten, ist i.d.R. aber schneller als eine Anfrage an eine externe API

Object Cache

Deutlich schneller als Transiente ist der Object Cache. Allerdings ist dieser Zwischenspeicher i.d.R. ein nicht persistenter Zwischenspeicher. Das heißt, er ist nur für den aktuellen Seitenaufruf verfügbar. Außer man stellt über ein spezielles Plugin einen persistenten Zwischenspeicher z.B. Redis oder Memcache bereit.

Entwickelt ihr ein Plugin, dass ihr auch anderen Nutzern bereitstellen wollt, solltet ihr davon ausgehen, dass es in deren WordPress-Installation nicht der Fall ist und lieber die Transient-API nutzen. Übrigens werden bei einem persistenten Zwischenspeicher i.d.R. auch die Transiente dann im Object Cache gespeichert und blähen die Datenbank nicht unnötig auf.

$request = [ 'id' => 123, 'type' => 'post', ]; $cache_key = esc_attr( implode( '_', $request ) ); $cache_group = 'my_plugin_xy'; if ( false === ( $result = wp_cache_get( $cache_key, $cache_group ) ) ) { $response = wp_remote_get( 'https://go-around.de/wp-json/' ); $result = json_decode( $response['body'] ); wp_cache_set( $cache_key, $result, $cache_group, HOUR_IN_SECONDS ); } // Do future work with $result
Code-Sprache: PHP (php)

Timeout setzen

Wenn ihr euer Plugin schreibt, funktioniert die genutzte API i.d.R. perfekt, antwortet schnell und beantwortet jede Anfrage. Ihr müsst aber davon ausgehen, dass ein Tag kommen wir, und der wird garantiert kommen, wo der angesprochene Server nicht erreichbar sein wird oder viel länger als üblich braucht um zu antworten.

Habt ihr diesen Fall nicht bedacht, führt das im schlimmsten Fall dazu, dass eure Besucher ebenfalls eine Fehlermeldung erhalten, weil der Server zu lange für die Antwort braucht.

Deswegen solltet ihr auf jeden Fall ein Timeout setzen. Standardmäßig ist von WordPress ein Timeout von 5 Sekunden vorgegeben. 5 Sekunden können aber trotzdem verdammt lang sein, wenn der Request beim Seitenaufruf durchgeführt wird. Ich empfehle für nicht kritische Informationen eher ein Timeout von 2 bis 3 Sekunden. Das ist immer davon abhängig, wie lange der Endpoint üblicherweise braucht um zu antworten.

$response = wp_remote_get( 'https://go-around.de/wp-json/', [ 'timeout' => 2 ] )
Code-Sprache: PHP (php)

Übrigens wird das Timeout pro Request angewendet, habt ihr mehrere Requests pro Seitenaufruf, kann sich das schnell aufsummieren und zu einem weiteren Problem führen:

Mehrfache Timeouts verhindern

Führt ihr pro Seitenaufruf mehrere Requests durch, können sich die Timeouts schnell aufsummieren und dazu führen, dass euer Webserver selber eurem Besucher eine HTTP 408 Request Timeout Fehler zurückgibt, weil PHP zu lange für die Antwort braucht.

Außerdem kann euer Server i.d.R. nur eine begrenzte Anzahl an parallelen Anfragen von euren Besuchern gleichzeitig verarbeiten. Solange PHP auf die Antwort der API wartet, können keine weiteren Anfragen von anderen Besuchern beantwortet werden, was schon zu einem HTTP 429 Too Many Requests Fehler führen kann.

Wirklich sorgen solltet ihr euch nur machen, wenn ihr pro Seitenaufruf mehrere Requests an eine API durchführt.

Es gibt mehrere Wege wie ihr weitere Requests verhindern könnt. Entweder für diesen einen Seitenaufruf oder für eine gewisse Zeitspanne für alle Seitenaufrufe. Hilfreich, z.B. wenn die von euch genutzte API nur eine bestimmte Anzahl an Requests pro Sekunde, Minute, Stunde… erlaubt.

Konstante

Bevor ihr euren Request losschickt, könnt ihr überprüfen, ob eine selber definierte Konstante gesetzt ist. Das funktioniert Funktionsübergreifend und gilt dann für den aktuellen Seitenaufruf:

if ( defined( 'MY_REQUEST_XY_EXCEEDED' ) ) { return false; } $response = wp_remote_get( 'https://go-around.de/wp-json/', [ 'timeout' => 2 ] ) if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { define( 'MY_REQUEST_XY_EXCEEDED', true ); return false; }
Code-Sprache: PHP (php)

Ihr könnt mit Hilfe von wp_remote_retrieve_response_code entweder überprüfen, ob die Antwort kein HTTP 200 OK Status zurückliefert oder auch spezielle HTTP Fehler, z.B. HTTP 408 Request Timeout oder HTTP 429 Too Many Requests, abfangen.

Transient

Wenn ihr weitere Requests an einen API Endpoint für eine bestimmte Zeit verhindert wollt, z.B. weil die API nur eine bestimmte Anzahl an Requests pro Sekunde, Minute, Stunde, Tag, Woche, Monat… erlaubt, solltet ihr eine Transiente setzen. Diese standardisierte API dient eigentlich dazu Daten für eine bestimmte Zeit zu speichern – siehe Ergebnisse Zwischenspeichern – kann aber auch dafür missbraucht werden weitere Requests für einen definierten Zeitraum zu unterbinden, denn Transiente laufen nach einer bei der Erstellung definierten Zeitraum ab.

if ( false !== get_transient( 'my_request_xy_exceeded' ) { return false; } $response = wp_remote_get( 'https://go-around.de/wp-json/', [ 'timeout' => 2 ] ) if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { set_transient( 'my_request_xy_exceeded', true, MINUTE_IN_SECONDS ); return false; }
Code-Sprache: PHP (php)

Im Gegensatz zu einer Konstante wird eine Transiente in der Datenbank oder Cache gespeichert und ist dort in diesem Beispiel für eine Minute MINUTE_IN_SECONDS definiert. Danach wird sie gelöscht und der Request kann wieder durchgeführt werden.

Fehlermeldungen abfangen

Nicht vergessen sollte man auch Fehlermeldungen der API abzufangen.

Mit wp_remote_retrieve_response_code kann man prüfen, welchen HTTP Response Code die Anfrage zurückliefert. Meistens will man ein HTTP 200 OK zurückerhalten aber auch andere 20X Statuscodes können in Ordnung sein. 4XX oder 5XX Fehler sind aber i.d.R. schlecht und sollten abgefangen werden.

if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { define( 'MY_REQUEST_XY_EXCEEDED', true ); return false; }
Code-Sprache: PHP (php)

Mit is_wp_error können wir überprüfen, ob schon vor dem Versenden der Anfrage in WordPress ein WP_Error aufgetreten ist. Dann können wir z.B. den Fehler ausgeben (sollte man nicht auf dem Live-Server tun!)

if ( is_wp_error( $response ) ) { return $response->get_error_message(); }
Code-Sprache: PHP (php)

Ggf. gibt aber auch die API selber einen Fehler zurück. Das ist aber sehr von der Implementierung abhängig. Die WordPress REST API gibt z.B. eine Fehlermeldung so zurück:

{ "code": "rest_no_route", "message": "Es wurde keine Route gefunden, die mit der URL und der Request-Methode identisch ist.", "data": { "status": 404 } }
Code-Sprache: JSON / JSON mit Kommentaren (json)

Fazit

Daten von Dritten zu beziehen, kann den eigenen Content deutlich aufwerten, birgt aber auch Probleme und kann im schlimmsten Fall dazu führen, dass die eigene Website nicht mehr erreichbar ist. Das sollte man bei der Programmierung verhindern!

Ein Kommentar zu “WordPress: Remote HTTP Request vor Timeouts absichern

Erwähnungen

  • Teilnehmerinnen und Teilnehmer sowie alle Beiträge zur #Projekt26 Challenge im Jahr 2020

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.