Pedido cURL em PHP: GET, POST, cabeçalhos e gestão de erros

Pedido cURL em PHP: GET, POST, cabeçalhos e gestão de erros
Resposta rápida

curl_init(), curl_setopt_array() com CURLOPT_RETURNTRANSFER e depois curl_exec(), cujo retorno tem de ser sempre testado. Desde o PHP 8.5, curl_close() está depreciada: o handle é um objeto libertado automaticamente. Um código HTTP 404 ou 500 não é um erro do cURL, é preciso ler CURLINFO_RESPONSE_CODE à parte.

O cURL é a forma mais direta de chamar uma API a partir do PHP. A extensão está presente em quase todos os alojamentos e trata do HTTP, dos redirecionamentos, dos cookies e do TLS. Este tutorial percorre os casos correntes, GET, POST, cabeçalhos, cookies, token Bearer, com a gestão de erros que a maior parte dos exemplos esquece, e assinala o que mudou no PHP 8.5.

O que mudou: curl_close() está depreciada

Desde o PHP 8.0, curl_init() já não devolve um recurso, mas um objeto CurlHandle, libertado automaticamente quando a variável sai de âmbito. curl_close() deixou portanto de ter efeito há cinco anos. O PHP 8.5 assume a situação e deprecia a função.

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

Medido em PHP 8.5.10 com cURL 8.14.1. Em PHP 8.4.25, a mesma linha não produz aviso nenhum: o código continua válido num alojamento partilhado que tenha ficado no 8.4, mas mais vale retirar já estas chamadas do que voltar a elas mais tarde. Para libertar um handle antes do fim do script, unset($ch) faz o trabalho.

Um pedido GET

Os parâmetros do URL constroem-se com http_build_query(), que codifica os valores. Concatená-los à mão parte assim que um valor contém um espaço, um & ou um acento.

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 faz com que curl_exec() devolva a resposta em vez de a escrever na saída padrão. Sem isso, a resposta é impressa a meio da tua página.

Verificar sempre o retorno

É o esquecimento mais caro. curl_exec() devolve false quando a rede falha, e o script segue como se nada 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

Um json_decode($resultat) a seguir devolveria null e o erro apareceria três ecrãs mais à frente. Duas linhas de verificação chegam para o fazer aparecer onde nasceu.

Atenção: um código HTTP 404 ou 500 não é um erro do cURL. O pedido chegou ao destino, o servidor respondeu, curl_exec() devolve o corpo da página de erro. É preciso ler o código de resposta à parte, ou ativar CURLOPT_FAILONERROR para que o cURL trate as respostas 400 e acima como falhas.

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

A forma de CURLOPT_POSTFIELDS decide o tipo de conteúdo enviado, e é uma fonte de confusão permanente:

  • uma string (http_build_query()) segue em application/x-www-form-urlencoded, o formato de um formulário HTML clássico.
  • um array segue em multipart/form-data, com fronteiras MIME. É o formato dos envios de ficheiros, e muitas API recusam-no.

Para enviar JSON, o tipo tem de ser declarado explicitamente:

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

Acrescentar cabeçalhos

Os cabeçalhos passam-se num array de strings Nom: valeur, através de 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);

Um User-Agent explícito evita bloqueios: muitos servidores rejeitam o agente por omissão do cURL, ou a ausência de agente.

Autenticação por 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,
]);

Um token não se escreve no código-fonte. Vem de uma variável de ambiente ou de um cofre. E só viaja em HTTPS: num URL em http://, o token circula às claras.

Os cookies

CURLOPT_COOKIE envia cookies fixos. Para uma sessão que tenha de guardar os cookies entre vários pedidos, precisas de CURLOPT_COOKIEJAR e CURLOPT_COOKIEFILE, com um ficheiro temporário.

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, // escreve os cookies recebidos
    CURLOPT_COOKIEFILE     => $cookies, // devolve-os ao pedido seguinte
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);

// Segundo pedido: os cookies de sessão seguem sozinhos
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);

unlink($cookies);

Nunca desativar a verificação TLS

Perante um erro de certificado, a resposta encontrada nos fóruns é sempre a mesma:

php
// A não fazer
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

Estas duas linhas eliminam a única proteção contra a interceção. Qualquer intermediário na rede pode então ler e alterar as trocas, token de autenticação incluído. O erro significa quase sempre que o repositório de certificados da máquina está em falta ou desatualizado. A correção certa é apontar curl.cainfo para um ficheiro de certificados atualizado no php.ini, ou atualizar o pacote ca-certificates do sistema.

O esqueleto reutilizável

Em vez de copiar as mesmas opções por todo o lado, uma pequena função concentra as definições seguras.

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       => '', // aceita 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),
    );
}

Chamada sobre a API REST do WordPress em local, esta função dá:

code
HTTP 200, 294919 octets, 1.580 s

Vários pedidos em paralelo

Dez chamadas sequenciais de 200 ms custam dois segundos. As mesmas em paralelo custam a mais lenta das dez. curl_multi_* serve exatamente para isso.

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 andar a girar em vazio no processador
    }
} 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);

O curl_multi_select() é indispensável: sem ele, o ciclo ocupa um núcleo a 100 % durante todo o tempo dos pedidos.

Em três pontos da API REST do WordPress em local, medido três vezes depois de uma volta de aquecimento:

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

O ganho cresce com o número de chamadas e com a latência de cada uma. Em três pedidos locais já rápidos, o paralelo divide o tempo por dois, em dez chamadas a uma API remota, a diferença torna-se bem mais nítida.

Quando não usar o cURL diretamente

Para uma chamada pontual, o cURL faz o trabalho. Assim que o projeto encadeia integrações, uma biblioteca cliente como o Guzzle ou um cliente PSR-18 traz as retentativas, a gestão dos fluxos e os intercetores sem que os tenhas de reescrever. E se a extensão cURL não estiver disponível, file_get_contents() com um contexto de fluxo safa um GET simples, mas sem controlo fino dos tempos limite nem diagnóstico de erro aproveitável.

O cURL cobre as trocas pedido-resposta. Quando é o servidor que tem de empurrar dados por iniciativa própria, é preciso outro transporte: vê criar um servidor WebSocket em PHP. Para chamar a API de um serviço de envio de e-mails, vê enviar um e-mail com PHP. E para passar a resposta ao browser, passar variáveis de PHP para JavaScript.

Vê também a ligação a uma base de dados em PHP e o hub Desenvolvimento web.

Erros frequentes

curl_close() depreciada no PHP 8.5 Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Nenhum aviso em PHP 8.4.25. O handle é um objeto CurlHandle libertado sozinho, usa unset() se for preciso.
Retorno de curl_exec() por testar Se a rede falhar, a função devolve false e o script continua. Um json_decode(false) mais à frente dá null, e o erro aparece num sítio que nada tem a ver.n404 confundido com um erro do cURL | Uma resposta 404 é um sucesso para o cURL. Lê CURLINFO_RESPONSE_CODE, ou ativa CURLOPT_FAILONERROR.
POSTFIELDS em array Um array produz multipart/form-data, uma string produz application/x-www-form-urlencoded. Verificado com CURLINFO_HEADER_OUT. Muitas API recusam o primeiro.
CURLOPT_SSL_VERIFYPEER desativado Elimina a única proteção contra a interceção. A verdadeira causa é um repositório de certificados em falta ou desatualizado: corrige o curl.cainfo ou o pacote ca-certificates.ncurl_multi sem curl_multi_select | O ciclo ocupa um núcleo a 100 % durante todo o tempo dos pedidos.

APIcURLHTTPPHP

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.