Comment faire une requête cURL en PHP

Comment faire une requête cURL en PHP
Réponse rapide

curl_init(), curl_setopt_array() avec CURLOPT_RETURNTRANSFER, puis curl_exec() dont il faut toujours tester le retour. Depuis PHP 8.5, curl_close() est déprécié : le handle est un objet libéré automatiquement. Un code HTTP 404 ou 500 n'est pas une erreur cURL, il faut lire CURLINFO_RESPONSE_CODE séparément.

cURL est le moyen le plus direct d’appeler une API depuis PHP. L’extension est présente sur presque tous les hébergements, elle gère HTTP, les redirections, les cookies et TLS. Ce tutoriel reprend les cas courants, GET, POST, en-têtes, cookies, jeton Bearer, avec la gestion d’erreur que la plupart des exemples oublient, et signale ce qui a changé sur PHP 8.5.

Ce qui a changé : curl_close() est déprécié

Depuis PHP 8.0, curl_init() ne renvoie plus une ressource mais un objet CurlHandle, libéré automatiquement quand la variable sort de portée. curl_close() n’avait donc plus d’effet depuis cinq ans. PHP 8.5 acte la situation et déprécie la fonction.

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

Mesuré sur PHP 8.5.10 avec cURL 8.14.1. Sur PHP 8.4.25, la même ligne ne produit aucun avis : le code reste valide sur un hébergement mutualisé resté en 8.4, mais il vaut mieux retirer ces appels maintenant que d’y revenir plus tard. Pour libérer un handle avant la fin du script, unset($ch) fait le travail.

Une requête GET

Les paramètres d’URL se construisent avec http_build_query(), qui encode les valeurs. Les concaténer à la main casse dès qu’une valeur contient un espace, un & ou un accent.

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 à true fait renvoyer la réponse par curl_exec() au lieu de l’écrire sur la sortie standard. Sans lui, la réponse s’imprime au milieu de votre page.

Toujours vérifier le retour

C’est l’oubli le plus coûteux. curl_exec() renvoie false en cas d’échec réseau, et le script continue comme si de rien n’était.

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

Un json_decode($resultat) derrière renverrait null, et l’erreur se manifesterait trois écrans plus loin. Deux lignes de vérification suffisent à la faire remonter à sa source.

Attention : un code HTTP 404 ou 500 n’est pas une erreur cURL. La requête a abouti, le serveur a répondu, curl_exec() renvoie le corps de la page d’erreur. Il faut lire le code de réponse séparément, ou activer CURLOPT_FAILONERROR pour que cURL traite les réponses 400 et au-delà comme des échecs.

Une requête POST

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

La forme de CURLOPT_POSTFIELDS décide du type de contenu envoyé, et c’est une source de confusion permanente :

  • une chaîne (http_build_query()) part en application/x-www-form-urlencoded, le format d’un formulaire HTML classique.
  • un tableau part en multipart/form-data, avec des frontières MIME. C’est le format des envois de fichiers, et beaucoup d’API le refusent.

Pour envoyer du JSON, le type doit être déclaré explicitement :

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

Ajouter des en-têtes

Les en-têtes se passent en tableau de chaînes Nom: valeur, via CURLOPT_HTTPHEADER.

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

Un User-Agent explicite évite les blocages : beaucoup de serveurs rejettent l’agent par défaut de cURL, ou l’absence d’agent.

Authentification par jeton Bearer

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

Un jeton ne s’écrit pas dans le code source. Il vient d’une variable d’environnement ou d’un coffre. Et il ne voyage que sur HTTPS : sur une URL en http://, le jeton circule en clair.

Les cookies

CURLOPT_COOKIE envoie des cookies fixes. Pour une session qui doit conserver les cookies entre plusieurs requêtes, ce sont CURLOPT_COOKIEJAR et CURLOPT_COOKIEFILE qu’il faut, avec un fichier temporaire.

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, // écrit les cookies reçus
    CURLOPT_COOKIEFILE     => $cookies, // les renvoie à la requête suivante
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);

// Deuxième requête : les cookies de session partent tout seuls
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);

unlink($cookies);

Ne jamais désactiver la vérification TLS

Devant une erreur de certificat, la réponse trouvée sur les forums est toujours la même :

php
// À ne pas faire
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

Ces deux lignes suppriment la seule protection contre l’interception. N’importe quel intermédiaire du réseau peut alors lire et modifier les échanges, jeton d’authentification compris. L’erreur signifie presque toujours que le magasin de certificats de la machine est absent ou périmé. La bonne correction est de pointer curl.cainfo vers un fichier de certificats à jour dans php.ini, ou de mettre à jour le paquet ca-certificates du système.

Le squelette réutilisable

Plutôt que de recopier les mêmes options partout, une petite fonction concentre les réglages sûrs.

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

Appelée sur l’API REST de WordPress en local, cette fonction donne :

code
HTTP 200, 294919 octets, 1.580 s

Plusieurs requêtes en parallèle

Dix appels séquentiels de 200 ms coûtent deux secondes. Les mêmes en parallèle coûtent le plus lent des dix. curl_multi_* sert exactement à ça.

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); // évite de tourner à vide sur le processeur
    }
} 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);

Le curl_multi_select() est indispensable : sans lui, la boucle occupe un cœur à 100 % pendant toute la durée des requêtes.

Sur trois points de l’API REST de WordPress en local, mesuré trois fois après un tour de chauffe :

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

Le gain grandit avec le nombre d’appels et avec la latence de chacun. Sur trois requêtes locales déjà rapides, le parallèle divise le temps par deux, sur dix appels à une API distante, l’écart devient bien plus net.

Quand ne pas utiliser cURL directement

Pour un appel ponctuel, cURL fait le travail. Dès que le projet enchaîne les intégrations, une bibliothèque cliente comme Guzzle ou un client PSR-18 apporte les réessais, la gestion des flux et les intercepteurs sans qu’on les réécrive. Et si l’extension cURL n’est pas disponible, file_get_contents() avec un contexte de flux dépanne pour un GET simple, mais sans délai fin ni diagnostic d’erreur exploitable.

cURL couvre les échanges requête-réponse. Quand le serveur doit pousser des données de lui-même, il faut un autre transport : voir créer un serveur WebSocket en PHP. Pour appeler l’API d’un service d’envoi d’e-mails, voir envoyer un e-mail avec PHP. Et pour transmettre la réponse au navigateur, passer des variables de PHP à JavaScript.

Voir aussi la connexion à une base de données en PHP et le hub Développement web.

Erreurs fréquentes

curl_close() déprécié en PHP 8.5 Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Aucun avis sur PHP 8.4.25. Le handle est un objet CurlHandle libéré tout seul, utilisez unset() si nécessaire.
Retour de curl_exec() non testé En cas d'échec réseau la fonction renvoie false et le script continue. Un json_decode(false) plus loin donne null, et l'erreur remonte à un endroit qui n'a rien à voir.n404 confondu avec une erreur cURL | Une réponse 404 est un succès pour cURL. Lire CURLINFO_RESPONSE_CODE, ou activer CURLOPT_FAILONERROR.
POSTFIELDS en tableau Un tableau produit multipart/form-data, une chaîne produit application/x-www-form-urlencoded. Vérifié avec CURLINFO_HEADER_OUT. Beaucoup d'API refusent le premier.
CURLOPT_SSL_VERIFYPEER désactivé Supprime la seule protection contre l'interception. La vraie cause est un magasin de certificats absent ou périmé : corrigez curl.cainfo ou le paquet ca-certificates.ncurl_multi sans curl_multi_select | La boucle occupe un cœur à 100 % pendant toute la durée des requêtes.

APIcURLHTTPPHP

Damien Flandrin Développeur web depuis 2010, créateur de Gekkode et d’Email Impact. Chaque article est testé sur un projet réel avant publication. Contact
Newsletter

Les nouveaux tests, tutoriels et projets, par e-mail.

Tests reproductibles, code versionné, résultats datés. Jamais de spam.