Zapytanie cURL w PHP: GET, POST, nagłówki i obsługa błędów

Zapytanie cURL w PHP: GET, POST, nagłówki i obsługa błędów
Szybka odpowiedź

curl_init(), curl_setopt_array() z CURLOPT_RETURNTRANSFER, a potem curl_exec(), którego zwracaną wartość trzeba zawsze sprawdzić. Od PHP 8.5 curl_close() jest przestarzałe: uchwyt to obiekt zwalniany automatycznie. Kod HTTP 404 czy 500 nie jest błędem cURL, trzeba osobno odczytać CURLINFO_RESPONSE_CODE.

cURL to najprostszy sposób, żeby odpytać API z poziomu PHP. Rozszerzenie jest dostępne na niemal każdym hostingu, obsługuje HTTP, przekierowania, ciasteczka i TLS. Ten poradnik przechodzi przez typowe przypadki, GET, POST, nagłówki, ciasteczka, token Bearer, razem z obsługą błędów, którą większość przykładów pomija, i pokazuje, co zmieniło się w PHP 8.5.

Co się zmieniło: curl_close() jest przestarzałe

Od PHP 8.0 curl_init() nie zwraca już zasobu, tylko obiekt CurlHandle, zwalniany automatycznie, kiedy zmienna wychodzi z zasięgu. curl_close() od pięciu lat nie robi więc nic. PHP 8.5 przyjmuje to do wiadomości i oznacza funkcję jako przestarzałą.

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

Zmierzone na PHP 8.5.10 z cURL 8.14.1. Na PHP 8.4.25 ta sama linia nie wywołuje żadnego ostrzeżenia: kod pozostaje poprawny na współdzielonym hostingu, który został przy 8.4, ale lepiej usunąć te wywołania teraz niż wracać do nich później. Żeby zwolnić uchwyt przed końcem skryptu, wystarczy unset($ch).

Zapytanie GET

Parametry adresu buduje się przez http_build_query(), które koduje wartości. Sklejanie ich ręcznie sypie się, gdy tylko wartość zawiera spację, & albo znak diakrytyczny.

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 ustawione na true sprawia, że curl_exec() zwraca odpowiedź, zamiast wypisywać ją na standardowe wyjście. Bez tego odpowiedź wyląduje w środku twojej strony.

Zawsze sprawdzaj zwracaną wartość

To najdroższe przeoczenie. curl_exec() zwraca false, kiedy sieć zawiedzie, a skrypt leci dalej, jakby nic się nie stało.

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

json_decode($resultat) kawałek dalej zwróciłoby null, a błąd ujawniłby się trzy ekrany później. Dwie linie sprawdzenia wystarczą, żeby wrócił do swojego źródła.

Uwaga: kod HTTP 404 albo 500 nie jest błędem cURL. Zapytanie doszło, serwer odpowiedział, a curl_exec() zwraca treść strony błędu. Kod odpowiedzi trzeba odczytać osobno albo włączyć CURLOPT_FAILONERROR, żeby cURL traktował odpowiedzi od 400 w górę jako niepowodzenie.

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

Postać CURLOPT_POSTFIELDS decyduje o typie wysyłanej treści i jest stałym źródłem nieporozumień:

  • ciąg znaków (http_build_query()) idzie jako application/x-www-form-urlencoded, czyli w formacie klasycznego formularza HTML.
  • tablica idzie jako multipart/form-data, z granicami MIME. To format wysyłki plików i wiele API go odrzuca.

Żeby wysłać JSON, typ trzeba zadeklarować wprost:

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

Dodawanie nagłówków

Nagłówki podaje się jako tablicę ciągów Nazwa: wartość, przez 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);

Jawny User-Agent pozwala uniknąć blokad: wiele serwerów odrzuca domyślny agent cURL albo jego brak.

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

Tokena nie zapisuje się w kodzie źródłowym. Pochodzi ze zmiennej środowiskowej albo z sejfu na sekrety. I podróżuje wyłącznie po HTTPS: pod adresem http:// token leci otwartym tekstem.

Ciasteczka

CURLOPT_COOKIE wysyła ustalone z góry ciasteczka. Do sesji, która ma zachowywać ciasteczka między kolejnymi zapytaniami, potrzebne są CURLOPT_COOKIEJAR i CURLOPT_COOKIEFILE oraz plik tymczasowy.

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, // zapisuje otrzymane ciasteczka
    CURLOPT_COOKIEFILE     => $cookies, // odsyła je przy następnym zapytaniu
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);

// Drugie zapytanie: ciasteczka sesji lecą same
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);

unlink($cookies);

Nigdy nie wyłączaj weryfikacji TLS

Na błąd certyfikatu odpowiedź znaleziona na forach jest zawsze ta sama:

php
// Tak nie rób
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

Te dwie linie usuwają jedyne zabezpieczenie przed przechwyceniem ruchu. Dowolny pośrednik w sieci może wtedy czytać i zmieniać wymianę, razem z tokenem uwierzytelniającym. Ten błąd prawie zawsze oznacza, że magazyn certyfikatów maszyny jest nieobecny albo przeterminowany. Właściwa poprawka to wskazanie curl.cainfo na aktualny plik certyfikatów w php.ini albo aktualizacja systemowego pakietu ca-certificates.

Szkielet do wielokrotnego użytku

Zamiast przepisywać te same opcje wszędzie, jedna mała funkcja skupia w sobie bezpieczne ustawienia.

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

Wywołana na lokalnym API REST WordPressa funkcja zwraca:

code
HTTP 200, 294919 octets, 1.580 s

Kilka zapytań równolegle

Dziesięć wywołań po 200 ms jedno po drugim kosztuje dwie sekundy. Te same równolegle kosztują tyle, co najwolniejsze z dziesięciu. curl_multi_* służy dokładnie do tego.

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); // zapobiega kręceniu się w pętli na pusto
    }
} 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() jest tu niezbędne: bez niego pętla zajmuje rdzeń w 100% przez cały czas trwania zapytań.

Na trzech punktach lokalnego API REST WordPressa, mierzone trzy razy po rozgrzewce:

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

Zysk rośnie wraz z liczbą wywołań i z opóźnieniem każdego z nich. Przy trzech lokalnych, już i tak szybkich zapytaniach równoległość skraca czas o połowę, przy dziesięciu wywołaniach do zdalnego API różnica robi się dużo wyraźniejsza.

Kiedy nie używać cURL bezpośrednio

Do pojedynczego wywołania cURL wystarczy. Gdy tylko projekt zaczyna łączyć kolejne integracje, biblioteka kliencka w rodzaju Guzzle albo klient PSR-18 daje ponawianie, obsługę strumieni i interceptory bez pisania ich od nowa. A jeśli rozszerzenie cURL nie jest dostępne, file_get_contents() z kontekstem strumienia ratuje sytuację przy prostym GET, ale bez precyzyjnych limitów czasu i bez użytecznej diagnostyki błędów.

cURL obsługuje wymianę typu zapytanie-odpowiedź. Kiedy to serwer ma wysyłać dane z własnej inicjatywy, potrzebny jest inny transport: zobacz jak stworzyć serwer WebSocket w PHP. Do wywołania API usługi wysyłającej e-maile zobacz wysyłanie e-maila w PHP. A żeby przekazać odpowiedź do przeglądarki, przekazywanie zmiennych z PHP do JavaScriptu.

Zobacz też połączenie z bazą danych w PHP oraz hub Programowanie webowe.

Częste błędy

curl_close() przestarzałe w PHP 8.5 Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Na PHP 8.4.25 żadnego ostrzeżenia. Uchwyt to obiekt CurlHandle zwalniany sam z siebie, w razie potrzeby użyj unset().
Niesprawdzona wartość zwracana przez curl_exec() Przy awarii sieci funkcja zwraca false, a skrypt leci dalej. json_decode(false) kawałek dalej daje null i błąd wychodzi w miejscu, które nie ma z nim nic wspólnego.n404 mylone z błędem cURL | Odpowiedź 404 jest dla cURL sukcesem. Odczytaj CURLINFO_RESPONSE_CODE albo włącz CURLOPT_FAILONERROR.
POSTFIELDS jako tablica Tablica daje multipart/form-data, ciąg znaków daje application/x-www-form-urlencoded. Sprawdzone przez CURLINFO_HEADER_OUT. Wiele API odrzuca to pierwsze.
Wyłączone CURLOPT_SSL_VERIFYPEER Usuwa jedyne zabezpieczenie przed przechwyceniem ruchu. Prawdziwą przyczyną jest brakujący albo przeterminowany magazyn certyfikatów: popraw curl.cainfo lub pakiet ca-certificates.ncurl_multi bez curl_multi_select | Pętla zajmuje rdzeń w 100% przez cały czas trwania zapytań.

APIcURLHTTPPHP

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty — e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.