cURL-Request in PHP: GET, POST, Header und Fehlerbehandlung

cURL-Request in PHP: GET, POST, Header und Fehlerbehandlung
Schnelle Antwort

curl_init(), curl_setopt_array() mit CURLOPT_RETURNTRANSFER, dann curl_exec(), dessen Rückgabewert du immer prüfen musst. Seit PHP 8.5 ist curl_close() deprecated: Das Handle ist ein Objekt und wird automatisch freigegeben. Ein HTTP-Status 404 oder 500 ist kein cURL-Fehler, den liest du separat über CURLINFO_RESPONSE_CODE aus.

cURL ist der direkteste Weg, aus PHP heraus eine API aufzurufen. Die Extension ist auf fast jedem Hosting vorhanden und beherrscht HTTP, Weiterleitungen, Cookies und TLS. Dieses Tutorial nimmt die üblichen Fälle durch, GET, POST, Header, Cookies, Bearer-Token, samt der Fehlerbehandlung, die die meisten Beispiele weglassen, und zeigt, was sich mit PHP 8.5 geändert hat.

Was sich geändert hat: curl_close() ist deprecated

Seit PHP 8.0 liefert curl_init() keine Ressource mehr, sondern ein Objekt vom Typ CurlHandle, das automatisch freigegeben wird, sobald die Variable ihren Gültigkeitsbereich verlässt. curl_close() hatte also seit fünf Jahren keine Wirkung mehr. PHP 8.5 zieht die Konsequenz und markiert die Funktion als deprecated.

php
$ch = curl_init();
var_dump(get_debug_type($ch));  // string(10) "CurlHandle"
var_dump(is_resource($ch));     // bool(false)

curl_close($ch);
code
Deprecated: Function curl_close() is deprecated since 8.5,
as it has no effect since PHP 8.0

Gemessen mit PHP 8.5.10 und cURL 8.14.1. Unter PHP 8.4.25 erzeugt dieselbe Zeile keinerlei Hinweis: Auf einem Shared Hosting, das noch auf 8.4 läuft, bleibt der Code gültig. Trotzdem entfernst du diese Aufrufe besser jetzt, als später darauf zurückzukommen. Um ein Handle vor dem Skriptende freizugeben, genügt unset($ch).

Ein GET-Request

URL-Parameter baust du mit http_build_query(), das die Werte kodiert. Von Hand zusammengesetzte Strings brechen, sobald ein Wert ein Leerzeichen, ein & oder ein Sonderzeichen enthält.

src/get.php
<?php

declare(strict_types=1);

$parametres = ['recherche' => 'café & thé', 'page' => 2];
$url = 'https://api.example.com/articles?' . http_build_query($parametres);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT        => 15,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_MAXREDIRS      => 3,
]);

$corps = curl_exec($ch);

if ($corps === false) {
    throw new RuntimeException('cURL : ' . curl_error($ch), curl_errno($ch));
}

$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
echo "HTTP {$code}, " . strlen($corps) . " octets\n";

CURLOPT_RETURNTRANSFER auf true sorgt dafür, dass curl_exec() die Antwort zurückgibt, statt sie auf die Standardausgabe zu schreiben. Ohne diese Option landet die Antwort mitten auf deiner Seite.

Den Rückgabewert immer prüfen

Das ist das teuerste Versäumnis. curl_exec() gibt bei einem Netzwerkfehler false zurück, und das Skript läuft weiter, als wäre nichts gewesen.

php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://hote-qui-nexiste-pas.invalid/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$resultat = curl_exec($ch);

var_dump($resultat);        // bool(false)
echo curl_errno($ch), "\n"; // 6
echo curl_error($ch), "\n"; // Could not resolve host: hote-qui-nexiste-pas.invalid

Ein json_decode($resultat) dahinter würde null liefern, und der Fehler tauchte drei Bildschirme weiter auf. Zwei Zeilen Prüfung genügen, damit er an seiner Quelle sichtbar wird.

Achtung: Ein HTTP-Status 404 oder 500 ist kein cURL-Fehler. Der Request ist durchgelaufen, der Server hat geantwortet, curl_exec() liefert den Rumpf der Fehlerseite. Du musst den Response-Code separat auslesen oder CURLOPT_FAILONERROR aktivieren, damit cURL Antworten ab 400 als Fehlschlag behandelt.

Ein POST-Request

src/post.php
$donnees = ['nom' => 'Damien', 'message' => 'Bonjour & merci'];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/contact',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query($donnees),
    CURLOPT_TIMEOUT        => 15,
]);

$reponse = curl_exec($ch);

Die Form von CURLOPT_POSTFIELDS entscheidet über den gesendeten Content-Type, und das ist eine Dauerquelle für Verwirrung:

  • ein String (http_build_query()) geht als application/x-www-form-urlencoded raus, das Format eines klassischen HTML-Formulars.
  • ein Array geht als multipart/form-data mit MIME-Grenzen raus. Das ist das Format für Datei-Uploads, und viele APIs lehnen es ab.

Um JSON zu senden, muss der Content-Type ausdrücklich gesetzt werden:

php
$charge = json_encode(['nom' => 'Damien'], JSON_THROW_ON_ERROR);

curl_setopt_array($ch, [
    CURLOPT_POST       => true,
    CURLOPT_POSTFIELDS => $charge,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Accept: application/json',
        'Content-Length: ' . strlen($charge),
    ],
]);

Header hinzufügen

Header übergibst du über CURLOPT_HTTPHEADER als Array von Strings der Form Name: Wert.

src/entetes.php
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/profil',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'X-Custom-Header: MaValeur',
        'Accept: application/json',
        'User-Agent: Gekkode/1.0 (+https://www.gekkode.com)',
    ],
]);

$reponse = curl_exec($ch);

Ein expliziter User-Agent verhindert Blockaden: Viele Server weisen den Standard-Agent von cURL oder einen fehlenden Agent ab.

Authentifizierung per Bearer-Token

php
$jeton = getenv('API_TOKEN') ?: throw new RuntimeException('API_TOKEN manquant');

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/data',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $jeton],
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);

Ein Token gehört nicht in den Quellcode. Es kommt aus einer Umgebungsvariable oder einem Vault. Und es reist ausschließlich über HTTPS: Bei einer URL mit http:// geht das Token im Klartext über die Leitung.

Cookies

CURLOPT_COOKIE sendet feste Cookies. Für eine Session, die Cookies über mehrere Requests hinweg behalten soll, brauchst du CURLOPT_COOKIEJAR und CURLOPT_COOKIEFILE mit einer temporären Datei.

src/session.php
$cookies = tempnam(sys_get_temp_dir(), 'ck');

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://example.com/connexion',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEJAR      => $cookies, // schreibt die empfangenen Cookies
    CURLOPT_COOKIEFILE     => $cookies, // schickt sie beim nächsten Request wieder mit
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);

// Zweiter Request: die Session-Cookies gehen von allein mit
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);

unlink($cookies);

Die TLS-Prüfung niemals abschalten

Bei einem Zertifikatsfehler lautet die Antwort in den Foren immer gleich:

php
// So nicht
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

Diese beiden Zeilen entfernen den einzigen Schutz gegen Abhören. Jede Zwischenstation im Netz kann den Verkehr dann mitlesen und verändern, das Auth-Token inklusive. Der Fehler bedeutet fast immer, dass der Zertifikatsspeicher der Maschine fehlt oder veraltet ist. Richtig behebst du ihn, indem du curl.cainfo in der php.ini auf eine aktuelle Zertifikatsdatei zeigen lässt oder das System-Paket ca-certificates aktualisierst.

Das wiederverwendbare Grundgerüst

Statt überall dieselben Optionen zu kopieren, bündelt eine kleine Funktion die sicheren Einstellungen.

src/Http.php
<?php

declare(strict_types=1);

final class ReponseHttp
{
    public function __construct(
        public readonly int $code,
        public readonly string $corps,
        public readonly float $duree,
    ) {}

    public function json(): array
    {
        return json_decode($this->corps, true, 512, JSON_THROW_ON_ERROR);
    }
}

function appel(
    string $url,
    string $methode = 'GET',
    ?string $corps = null,
    array $entetes = [],
    int $delai = 15,
): ReponseHttp {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $methode,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT        => $delai,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_HTTPHEADER     => $entetes,
        CURLOPT_ENCODING       => '', // akzeptiert gzip und deflate
    ]);

    if ($corps !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $corps);
    }

    $reponse = curl_exec($ch);

    if ($reponse === false) {
        throw new RuntimeException(
            sprintf('cURL %d sur %s : %s', curl_errno($ch), $url, curl_error($ch)),
            curl_errno($ch),
        );
    }

    return new ReponseHttp(
        code:  curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
        corps: $reponse,
        duree: curl_getinfo($ch, CURLINFO_TOTAL_TIME),
    );
}

Auf die lokale REST-API von WordPress angewendet, liefert diese Funktion:

code
HTTP 200, 294919 octets, 1.580 s

Mehrere Requests parallel

Zehn sequenzielle Aufrufe zu je 200 ms kosten zwei Sekunden. Dieselben parallel kosten so viel wie der langsamste der zehn. Genau dafür ist curl_multi_* da.

src/parallele.php
$urls = [
    'https://api.example.com/a',
    'https://api.example.com/b',
    'https://api.example.com/c',
];

$multi   = curl_multi_init();
$handles = [];

foreach ($urls as $i => $url) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
    ]);
    curl_multi_add_handle($multi, $ch);
    $handles[$i] = $ch;
}

do {
    $etat = curl_multi_exec($multi, $encoursDExecution);
    if ($encoursDExecution) {
        curl_multi_select($multi); // vermeidet Leerlauf auf dem Prozessor
    }
} while ($encoursDExecution && $etat === CURLM_OK);

$reponses = [];
foreach ($handles as $i => $ch) {
    $reponses[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);

curl_multi_select() ist unverzichtbar: Ohne diesen Aufruf belegt die Schleife einen Kern zu 100 %, solange die Requests laufen.

Auf drei Endpunkten der lokalen WordPress-REST-API, dreimal gemessen nach einem Aufwärmdurchlauf:

code
passe 1 : séquentiel  1367 ms | parallèle  459 ms
passe 2 : séquentiel   868 ms | parallèle  415 ms
passe 3 : séquentiel   733 ms | parallèle  408 ms

Der Gewinn wächst mit der Zahl der Aufrufe und mit der Latenz jedes einzelnen. Bei drei ohnehin schnellen lokalen Requests halbiert die Parallelisierung die Zeit, bei zehn Aufrufen an eine entfernte API wird der Abstand deutlich größer.

Wann du cURL nicht direkt verwenden solltest

Für einen einzelnen Aufruf erledigt cURL die Arbeit. Sobald ein Projekt Integration an Integration reiht, bringt eine Client-Bibliothek wie Guzzle oder ein PSR-18-Client die Retries, das Stream-Handling und die Interceptors mit, ohne dass du sie neu schreibst. Und wenn die cURL-Extension fehlt, hilft file_get_contents() mit einem Stream-Context bei einem einfachen GET aus, allerdings ohne feine Timeouts und ohne brauchbare Fehlerdiagnose.

cURL deckt den Austausch aus Anfrage und Antwort ab. Wenn der Server von sich aus Daten schicken soll, braucht es einen anderen Transport: siehe einen WebSocket-Server in PHP erstellen. Für den Aufruf der API eines E-Mail-Dienstes siehe eine E-Mail mit PHP versenden. Und um die Antwort an den Browser weiterzureichen, Variablen von PHP an JavaScript übergeben.

Siehe auch die Verbindung zu einer Datenbank in PHP und den Hub Webentwicklung.

Häufige Fehler

curl_close() in PHP 8.5 deprecated Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Unter PHP 8.4.25 kommt kein Hinweis. Das Handle ist ein Objekt vom Typ CurlHandle und wird von allein freigegeben, nimm bei Bedarf unset().
Rückgabewert von curl_exec() nicht geprüft Bei einem Netzwerkfehler gibt die Funktion false zurück und das Skript läuft weiter. Ein json_decode(false) weiter unten ergibt null, und der Fehler taucht an einer Stelle auf, die nichts damit zu tun hat.n404 mit einem cURL-Fehler verwechselt | Eine 404-Antwort ist für cURL ein Erfolg. Lies CURLINFO_RESPONSE_CODE aus oder aktiviere CURLOPT_FAILONERROR.
POSTFIELDS als Array Ein Array erzeugt multipart/form-data, ein String erzeugt application/x-www-form-urlencoded. Geprüft mit CURLINFO_HEADER_OUT. Viele APIs lehnen die erste Variante ab.
CURLOPT_SSL_VERIFYPEER abgeschaltet Entfernt den einzigen Schutz gegen Abhören. Die wahre Ursache ist ein fehlender oder veralteter Zertifikatsspeicher: Korrigiere curl.cainfo oder das Paket ca-certificates.ncurl_multi ohne curl_multi_select | Die Schleife belegt einen Kern zu 100 %, solange die Requests laufen.

APIcURLHTTPPHP

Damien Flandrin Webentwickler seit 2010, Gründer von Gekkode und Email Impact. Jeder Artikel wird vor der Veröffentlichung an einem echten Projekt getestet. Kontakt
Newsletter

Neue Tests, Tutorials und Projekte, per E-Mail.

Reproduzierbare Tests, versionierter Code, datierte Ergebnisse. Niemals Spam.