Richiesta cURL in PHP: GET, POST, header e gestione errori

Richiesta cURL in PHP: GET, POST, header e gestione errori
Risposta rapida

curl_init(), curl_setopt_array() con CURLOPT_RETURNTRANSFER, poi curl_exec() di cui va sempre verificato il valore restituito. Da PHP 8.5 curl_close() è deprecata: l'handle è un oggetto liberato automaticamente. Un codice HTTP 404 o 500 non è un errore cURL, il codice va letto a parte con CURLINFO_RESPONSE_CODE.

cURL è il modo più diretto per chiamare un’API da PHP. L’estensione è presente su quasi tutti gli hosting e gestisce HTTP, i redirect, i cookie e TLS. Questo tutorial copre i casi ricorrenti, GET, POST, header, cookie, token Bearer, con la gestione degli errori che la maggior parte degli esempi dimentica, e segnala che cosa è cambiato in PHP 8.5.

Che cosa è cambiato: curl_close() è deprecata

Da PHP 8.0 curl_init() non restituisce più una risorsa ma un oggetto CurlHandle, liberato automaticamente quando la variabile esce dallo scope. curl_close() non aveva quindi più alcun effetto da cinque anni. PHP 8.5 prende atto della situazione e deprecia la funzione.

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

Misurato su PHP 8.5.10 con cURL 8.14.1. Su PHP 8.4.25 la stessa riga non produce alcun avviso: il codice resta valido su un hosting condiviso rimasto alla 8.4, ma è meglio togliere queste chiamate adesso che tornarci più avanti. Per liberare un handle prima della fine dello script, unset($ch) fa il lavoro.

Una richiesta GET

I parametri dell’URL si costruiscono con http_build_query(), che codifica i valori. Concatenarli a mano si rompe non appena un valore contiene uno spazio, una & o una lettera accentata.

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 a true fa restituire la risposta da curl_exec() invece di scriverla sullo standard output. Senza, la risposta finisce stampata in mezzo alla tua pagina.

Verificare sempre il valore restituito

È la dimenticanza più costosa. curl_exec() restituisce false in caso di errore di rete, e lo script prosegue come se niente fosse.

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) subito dopo restituirebbe null, e l’errore si manifesterebbe tre schermate più in là. Due righe di controllo bastano a farlo emergere dove nasce.

Attenzione: un codice HTTP 404 o 500 non è un errore cURL. La richiesta è andata a buon fine, il server ha risposto, curl_exec() restituisce il corpo della pagina di errore. Il codice di risposta va letto a parte, oppure va attivato CURLOPT_FAILONERROR perché cURL tratti le risposte dalla 400 in su come fallimenti.

Una richiesta 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 forma di CURLOPT_POSTFIELDS decide il tipo di contenuto inviato, ed è una fonte di confusione permanente:

  • una stringa (http_build_query()) parte come application/x-www-form-urlencoded, il formato di un normale form HTML.
  • un array parte come multipart/form-data, con i boundary MIME. È il formato dell’invio di file, e molte API lo rifiutano.

Per inviare del JSON, il tipo va dichiarato esplicitamente:

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

Aggiungere degli header

Gli header si passano come array di stringhe Nome: valore, tramite 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);

Uno User-Agent esplicito evita i blocchi: molti server rifiutano l’agent predefinito di cURL, o la sua assenza.

Autenticazione con token 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 token non si scrive nel codice sorgente. Arriva da una variabile d’ambiente o da un vault. E viaggia solo su HTTPS: su un URL in http:// il token passa in chiaro.

CURLOPT_COOKIE invia cookie fissi. Per una sessione che deve conservare i cookie tra più richieste servono CURLOPT_COOKIEJAR e CURLOPT_COOKIEFILE, con un file temporaneo.

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, // scrive i cookie ricevuti
    CURLOPT_COOKIEFILE     => $cookies, // li rimanda alla richiesta successiva
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);

// Seconda richiesta: i cookie di sessione partono da soli
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);

unlink($cookies);

Non disattivare mai la verifica TLS

Davanti a un errore di certificato, la risposta che si trova sui forum è sempre la stessa:

php
// Da non fare
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

Queste due righe eliminano l’unica protezione contro l’intercettazione. Qualsiasi intermediario di rete può allora leggere e modificare gli scambi, token di autenticazione compreso. L’errore significa quasi sempre che l’archivio dei certificati della macchina è assente o scaduto. La correzione giusta è far puntare curl.cainfo a un file di certificati aggiornato in php.ini, oppure aggiornare il pacchetto ca-certificates del sistema.

Lo scheletro riutilizzabile

Invece di ricopiare le stesse opzioni ovunque, una piccola funzione concentra le impostazioni sicure.

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

Chiamata sull’API REST di WordPress in locale, questa funzione restituisce:

code
HTTP 200, 294919 octets, 1.580 s

Più richieste in parallelo

Dieci chiamate sequenziali da 200 ms costano due secondi. Le stesse in parallelo costano quanto la più lenta delle dieci. curl_multi_* serve esattamente a questo.

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); // evita di girare a vuoto sul processore
    }
} 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);

Il curl_multi_select() è indispensabile: senza, il ciclo tiene un core al 100 % per tutta la durata delle richieste.

Su tre endpoint dell’API REST di WordPress in locale, misurato tre volte dopo un giro di riscaldamento:

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

Il guadagno cresce con il numero di chiamate e con la latenza di ciascuna. Su tre richieste locali già veloci il parallelo dimezza il tempo, su dieci chiamate a un’API remota lo scarto diventa molto più netto.

Quando non usare cURL direttamente

Per una chiamata occasionale cURL fa il suo lavoro. Non appena il progetto accumula integrazioni, una libreria client come Guzzle o un client PSR-18 porta i retry, la gestione degli stream e gli interceptor senza doverli riscrivere. E se l’estensione cURL non è disponibile, file_get_contents() con un contesto di stream toglie d’impaccio per un GET semplice, ma senza timeout fini né diagnostica d’errore utilizzabile.

cURL copre gli scambi richiesta-risposta. Quando è il server a dover inviare dati di sua iniziativa serve un altro trasporto: vedi creare un server WebSocket in PHP. Per chiamare l’API di un servizio di invio e-mail, vedi inviare un’e-mail con PHP. E per passare la risposta al browser, passare variabili da PHP a JavaScript.

Vedi anche la connessione a un database in PHP e l’hub Sviluppo web.

Errori frequenti

curl_close() deprecata in PHP 8.5 Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Nessun avviso su PHP 8.4.25. L'handle è un oggetto CurlHandle liberato da solo, usa unset() se serve.
Valore restituito da curl_exec() non verificato In caso di errore di rete la funzione restituisce false e lo script prosegue. Un json_decode(false) più avanti dà null, e l'errore emerge in un punto che non c'entra niente.n404 confuso con un errore cURL | Una risposta 404 per cURL è un successo. Leggi CURLINFO_RESPONSE_CODE, oppure attiva CURLOPT_FAILONERROR.
POSTFIELDS passato come array Un array produce multipart/form-data, una stringa produce application/x-www-form-urlencoded. Verificato con CURLINFO_HEADER_OUT. Molte API rifiutano il primo.
CURLOPT_SSL_VERIFYPEER disattivato Elimina l'unica protezione contro l'intercettazione. La vera causa è un archivio di certificati assente o scaduto: correggi curl.cainfo o il pacchetto ca-certificates.ncurl_multi senza curl_multi_select | Il ciclo tiene un core al 100 % per tutta la durata delle richieste.

APIcURLHTTPPHP

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.