Peticiones HTTP en PHP con cURL: GET, POST y errores

Peticiones HTTP en PHP: para hablar con una API desde PHP, la extensión cURL viene en casi todas las instalaciones y lo permite todo. Lo que hay que saber es configurarla bien y distinguir un error de red de una respuesta de error.

Los ejemplos de esta página hablan con un pequeño servidor de pruebas que arrancamos en nuestra máquina, en http://localhost:8000, para que las respuestas sean siempre las mismas. Con cualquier API real funciona igual cambiando la dirección. No se pueden probar en el editor del navegador, que no tiene acceso a la red.

GET y JSON

<?php
declare(strict_types=1);

$ch = curl_init('http://localhost:8000/productos');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);

$cuerpo = curl_exec($ch);
$codigo = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($cuerpo === false) {
    throw new RuntimeException('Fallo de red: ' . curl_error($ch));
}

echo "Código $codigo\n";
foreach (json_decode($cuerpo, true, flags: JSON_THROW_ON_ERROR) as $p) {
    echo "- {$p['nombre']}: {$p['precio']}\n";
}
salidaCódigo 200
- Café: 12.5
- Taza: 8

curl_init crea la petición, curl_setopt_array la configura y curl_exec la envía. Sin CURLOPT_RETURNTRANSFER, la respuesta se imprimiría directamente en vez de devolverse. curl_exec devuelve false solo si falló la red (no hay conexión, el nombre no existe, se agotó el tiempo); el código HTTP se pregunta aparte con curl_getinfo. Desde PHP 8 no hace falta curl_close: la petición se libera sola.

POST con JSON y cabeceras

<?php
declare(strict_types=1);

$datos = ['nombre' => 'Ana', 'edad' => 31];

$ch = curl_init('http://localhost:8000/eco');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($datos, JSON_THROW_ON_ERROR),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . (getenv('API_TOKEN') ?: 'token-de-prueba'),
    ],
]);

$respuesta = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), "\n";
echo $respuesta, "\n";
salida201
{"recibido":{"nombre":"Ana","edad":31},"tipo":"application\/json","token":"Bearer token-de-prueba"}

Para enviar JSON, el cuerpo se codifica con json_encode y se avisa con la cabecera Content-Type. El servidor de prueba devuelve lo que recibió, con un 201 («creado»). La clave de la API se lee de una variable de entorno, nunca escrita en el código: lo explicamos en configuración con variables de entorno.

Un 404 no es un error de cURL

<?php
declare(strict_types=1);

function pedir(string $url): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
    $cuerpo = curl_exec($ch);
    if ($cuerpo === false) {
        throw new RuntimeException(curl_error($ch));
    }
    $codigo = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($codigo >= 400) {
        throw new RuntimeException("El servidor respondió $codigo: $cuerpo");
    }
    return json_decode($cuerpo, true, flags: JSON_THROW_ON_ERROR);
}

try {
    pedir('http://localhost:8000/no-existe');
} catch (RuntimeException $e) {
    echo $e->getMessage(), "\n";
}
salidaEl servidor respondió 404: {"error": "No existe"}

El tiempo de espera

<?php
declare(strict_types=1);

$ch = curl_init('http://localhost:8000/lento');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 1]);

$inicio = microtime(true);
$cuerpo = curl_exec($ch);
printf("%s tras %.1f s\n", $cuerpo === false ? 'Error: ' . curl_error($ch) : 'Respuesta', microtime(true) - $inicio);
salidaError: Operation timed out after 1002 milliseconds with 0 bytes received tras 1.0 s
Sin límite por omisión. Si no pones CURLOPT_TIMEOUT, cURL espera indefinidamente a un servidor que no contesta, y tu página se queda colgada con él. Pon siempre un tiempo máximo (CURLOPT_TIMEOUT) y otro para conectar (CURLOPT_CONNECTTIMEOUT). Y si la URL la elige un usuario, valídala antes: lo explicamos en SSRF, en los fallos de seguridad típicos.

¿Y file_get_contents?

file_get_contents('https://…') también descarga una URL y vale para un caso rápido, pero cuesta más controlar el tiempo de espera, las cabeceras y los errores. Para una aplicación, cURL o una biblioteca como Guzzle (composer require guzzlehttp/guzzle), que lo envuelve en una interfaz más cómoda. Si la usas, mantenla al día: en la lección de Composer vimos cómo composer audit encontró 14 avisos de seguridad en una versión antigua.

Resumen

ParaUsa
Devolver la respuestaCURLOPT_RETURNTRANSFER => true
Enviar JSONCURLOPT_POSTFIELDS => json_encode(...) y Content-Type
Código de estadocurl_getinfo($ch, CURLINFO_RESPONSE_CODE)
Error de redcurl_exec devuelve false; curl_error($ch)
Limitar la esperaCURLOPT_TIMEOUT y CURLOPT_CONNECTTIMEOUT

Todo el código de esta página se ejecutó con PHP 8.5.12 en un contenedor limpio, contra un servidor de pruebas propio arrancado con php -S dentro del mismo contenedor y sin acceso a internet; las salidas están copiadas de esa ejecución.