cURL-request in PHP: GET, POST, headers en foutafhandeling

cURL-request in PHP: GET, POST, headers en foutafhandeling
Kort antwoord

curl_init(), curl_setopt_array() met CURLOPT_RETURNTRANSFER, daarna curl_exec(), waarvan je de returnwaarde altijd moet testen. Sinds PHP 8.5 is curl_close() deprecated: de handle is een object dat vanzelf wordt vrijgegeven. Een HTTP-code 404 of 500 is geen cURL-fout, je leest CURLINFO_RESPONSE_CODE apart uit.

cURL is de meest directe manier om vanuit PHP een API aan te roepen. De extensie staat op vrijwel elke hosting en regelt HTTP, redirects, cookies en TLS. Deze tutorial neemt de gangbare gevallen door, GET, POST, headers, cookies, Bearer-token, met de foutafhandeling die de meeste voorbeelden weglaten, en wijst aan wat er met PHP 8.5 is veranderd.

Wat er is veranderd: curl_close() is deprecated

Sinds PHP 8.0 geeft curl_init() geen resource meer terug maar een object CurlHandle, dat vanzelf wordt vrijgegeven zodra de variabele buiten scope raakt. curl_close() had dus al vijf jaar geen effect meer. PHP 8.5 trekt daar de conclusie uit en markeert de functie 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

Gemeten op PHP 8.5.10 met cURL 8.14.1. Op PHP 8.4.25 levert dezelfde regel geen enkele melding op: de code blijft geldig op een shared hosting die op 8.4 is blijven staan, maar je haalt die aanroepen beter nu weg dan er later op terug te komen. Wil je een handle vrijgeven voor het einde van het script, dan doet unset($ch) het werk.

Een GET-request

URL-parameters bouw je met http_build_query(), dat de waarden encodeert. Ze met de hand aan elkaar plakken breekt zodra een waarde een spatie, een & of een accent bevat.

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 op true zorgt dat curl_exec() het antwoord teruggeeft in plaats van het naar de standaarduitvoer te schrijven. Zonder die optie belandt het antwoord midden in je pagina.

Test altijd de returnwaarde

Dit is de duurste vergetelheid. curl_exec() geeft false terug bij een netwerkfout, en het script loopt door alsof er niets aan de hand is.

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

Een json_decode($resultat) erachter zou null opleveren, en de fout zou pas drie schermen verderop opduiken. Twee regels controle zijn genoeg om hem bij de bron te laten opspelen.

Let op: een HTTP-code 404 of 500 is geen cURL-fout. Het request is geslaagd, de server heeft geantwoord, curl_exec() geeft de body van de foutpagina terug. Je moet de responscode apart uitlezen, of CURLOPT_FAILONERROR aanzetten zodat cURL antwoorden vanaf 400 als mislukking behandelt.

Een 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);

De vorm van CURLOPT_POSTFIELDS bepaalt welk contenttype er vertrekt, en dat is een blijvende bron van verwarring:

  • een string (http_build_query()) gaat weg als application/x-www-form-urlencoded, het formaat van een klassiek HTML-formulier.
  • een array gaat weg als multipart/form-data, met MIME-boundaries. Dat is het formaat voor bestandsuploads, en veel API’s weigeren het.

Om JSON te versturen moet het type expliciet worden opgegeven:

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),
    ],
]);

Headers toevoegen

Headers geef je via CURLOPT_HTTPHEADER mee als array van strings in de vorm Naam: waarde.

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);

Een expliciete User-Agent voorkomt blokkades: veel servers weigeren de standaardagent van cURL, of het ontbreken van een agent.

Authenticatie met een 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,
]);

Een token schrijf je niet in de broncode. Hij komt uit een omgevingsvariabele of uit een vault. En hij reist alleen over HTTPS: op een URL met http:// gaat het token leesbaar over de lijn.

Cookies

CURLOPT_COOKIE stuurt vaste cookies mee. Voor een sessie die de cookies over meerdere requests moet bewaren heb je CURLOPT_COOKIEJAR en CURLOPT_COOKIEFILE nodig, met een tijdelijk bestand.

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, // schrijft de ontvangen cookies weg
    CURLOPT_COOKIEFILE     => $cookies, // stuurt ze mee met het volgende request
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);

// Tweede request: de sessiecookies gaan vanzelf mee
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);

unlink($cookies);

Zet de TLS-verificatie nooit uit

Bij een certificaatfout is het antwoord op de forums altijd hetzelfde:

php
// Zo moet het niet
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

Die twee regels halen de enige bescherming tegen onderschepping weg. Elke tussenpartij op het netwerk kan het verkeer dan lezen en aanpassen, authenticatietoken inbegrepen. De fout betekent bijna altijd dat de certificaatstore van de machine ontbreekt of verlopen is. De juiste oplossing is curl.cainfo in php.ini naar een actueel certificaatbestand laten wijzen, of het systeempakket ca-certificates bijwerken.

Het herbruikbare skelet

In plaats van overal dezelfde opties over te tikken, bundelt een kleine functie de veilige instellingen.

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       => '', // accepteert gzip en 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),
    );
}

Aangeroepen op de lokale REST API van WordPress geeft deze functie:

code
HTTP 200, 294919 octets, 1.580 s

Meerdere requests parallel

Tien opeenvolgende aanroepen van 200 ms kosten twee seconden. Dezelfde aanroepen parallel kosten de traagste van de tien. Daar dient curl_multi_* precies voor.

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); // voorkomt dat de processor leeg draait
    }
} 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);

De curl_multi_select() is onmisbaar: zonder die aanroep houdt de lus een core op 100 % bezig zolang de requests duren.

Op drie endpoints van de lokale REST API van WordPress, drie keer gemeten na een warmdraaironde:

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

De winst groeit met het aantal aanroepen en met de latency van elk ervan. Op drie lokale requests die al snel zijn, halveert parallel de tijd, bij tien aanroepen naar een externe API wordt het verschil veel duidelijker.

Wanneer je cURL niet rechtstreeks gebruikt

Voor een losse aanroep doet cURL het werk. Zodra een project integratie op integratie stapelt, levert een clientbibliotheek als Guzzle of een PSR-18-client de retries, het streambeheer en de interceptors zonder dat je ze zelf hoeft te schrijven. En is de cURL-extensie niet beschikbaar, dan helpt file_get_contents() met een stream context je uit de brand voor een simpele GET, maar zonder fijnmazige timeouts en zonder bruikbare foutdiagnose.

cURL dekt de uitwisseling van request en response. Moet de server uit zichzelf data pushen, dan heb je een ander transport nodig: zie een WebSocket-server maken in PHP. Voor het aanroepen van de API van een dienst die e-mail verstuurt, zie een e-mail versturen met PHP. En om het antwoord door te geven aan de browser: variabelen van PHP naar JavaScript doorgeven.

Zie ook de verbinding met een database in PHP en de hub Webdevelopment.

Veelgemaakte fouten

curl_close() deprecated in PHP 8.5 Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Geen enkele melding op PHP 8.4.25. De handle is een object CurlHandle dat vanzelf wordt vrijgegeven, gebruik unset() als het nodig is.
Returnwaarde van curl_exec() niet getest Bij een netwerkfout geeft de functie false terug en loopt het script door. Een json_decode(false) verderop levert null op, en de fout duikt op een plek op die er niets mee te maken heeft.n404 verward met een cURL-fout | Een 404-antwoord is voor cURL een succes. Lees CURLINFO_RESPONSE_CODE, of zet CURLOPT_FAILONERROR aan.
POSTFIELDS als array Een array levert multipart/form-data op, een string application/x-www-form-urlencoded. Gecontroleerd met CURLINFO_HEADER_OUT. Veel API's weigeren het eerste.
CURLOPT_SSL_VERIFYPEER uitgezet Haalt de enige bescherming tegen onderschepping weg. De echte oorzaak is een ontbrekende of verlopen certificaatstore: corrigeer curl.cainfo of het pakket ca-certificates.ncurl_multi zonder curl_multi_select | De lus houdt een core op 100 % bezig zolang de requests duren.

APIcURLHTTPPHP

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.