Saltar al contenido principal

Clase 07 — curl en Scripts de Bash

Principios para scripting con curl

  1. Siempre usar -s (silence) para quitar barras de progreso
  2. Capturar código HTTP con -w "%{http_code}"
  3. Separar body y código HTTP para manejo de errores
  4. Usar --max-time para evitar scripts colgados
  5. Usar --fail o verificar $? para detectar errores

Patrón: Capturar respuesta + código HTTP

# Método 1: Separar body y http_code
response=$(curl -s -w "\n%{http_code}" --max-time 10 https://httpbin.org/get)
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')

echo "HTTP: $http_code"
echo "$body" | jq '.url'

# Método 2: Body a archivo, code a variable
http_code=$(curl -s -o /tmp/response.json -w "%{http_code}" https://httpbin.org/get)
echo "HTTP: $http_code"
jq '.' /tmp/response.json

# Método 3: --fail (devuelve exit code != 0 si HTTP >= 400)
if curl -sf --max-time 10 https://httpbin.org/get > /tmp/data.json; then
echo "OK"
jq '.' /tmp/data.json
else
echo "Error (exit code: $?)"
fi

Funciones reutilizables

Función genérica de API

#!/bin/bash
# lib-api.sh - Librería de funciones para APIs

# Configuración
API_BASE_URL="${API_BASE_URL:-https://httpbin.org}"
API_TOKEN="${API_TOKEN:-}"
API_TIMEOUT="${API_TIMEOUT:-30}"

# Función base de petición
_api_request() {
local method="$1"
local endpoint="$2"
local data="$3"

local args=(
-s
-X "$method"
-H "Content-Type: application/json"
-H "Accept: application/json"
--max-time "$API_TIMEOUT"
-w "\n__STATUS__%{http_code}"
)

[[ -n "$API_TOKEN" ]] && args+=(-H "Authorization: Bearer $API_TOKEN")
[[ -n "$data" ]] && args+=(-d "$data")

local response
response=$(curl "${args[@]}" "${API_BASE_URL}${endpoint}")

local http_code
http_code=$(echo "$response" | grep "__STATUS__" | sed 's/__STATUS__//')
local body
body=$(echo "$response" | grep -v "__STATUS__")

# Exportar para uso externo
export LAST_HTTP_CODE="$http_code"
export LAST_RESPONSE="$body"

if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
echo "$body"
return 0
else
echo "Error HTTP $http_code: $body" >&2
return 1
fi
}

# Funciones de conveniencia
api_get() { _api_request GET "$1"; }
api_post() { _api_request POST "$1" "$2"; }
api_put() { _api_request PUT "$1" "$2"; }
api_patch() { _api_request PATCH "$1" "$2"; }
api_delete() { _api_request DELETE "$1"; }

Uso de la librería

#!/bin/bash
source lib-api.sh

# Configurar
export API_BASE_URL="https://jsonplaceholder.typicode.com"

# GET
api_get "/posts/1" | jq '.title'

# POST
api_post "/posts" '{"title":"Nuevo post","body":"Contenido","userId":1}' | jq '.id'

# Verificar código HTTP
api_get "/posts/1" > /dev/null
echo "Último código: $LAST_HTTP_CODE"

Reintentos con backoff exponencial

#!/bin/bash
# retry.sh - Petición con reintentos

curl_with_retry() {
local url="$1"
local max_retries="${2:-5}"
local retry_delay="${3:-2}"

for ((i=1; i<=max_retries; i++)); do
response=$(curl -s -o /tmp/curl_response.txt -w "%{http_code}" \
--max-time 10 "$url")

if [[ "$response" -ge 200 && "$response" -lt 300 ]]; then
cat /tmp/curl_response.txt
return 0
fi

if [[ "$response" -eq 429 || "$response" -ge 500 ]]; then
delay=$((retry_delay * i)) # Backoff exponencial
echo "⚠️ Intento $i/$max_retries falló (HTTP $response). Reintentando en ${delay}s..." >&2
sleep "$delay"
else
echo "❌ Error HTTP $response (no reintentable)" >&2
return 1
fi
done

echo "❌ Máximo de reintentos alcanzado ($max_retries)" >&2
return 1
}

# Uso
resultado=$(curl_with_retry "https://httpbin.org/get")
echo "$resultado" | jq '.'

Peticiones en paralelo

#!/bin/bash
# parallel-requests.sh

urls=(
"https://httpbin.org/delay/1"
"https://httpbin.org/delay/2"
"https://httpbin.org/delay/1"
"https://httpbin.org/delay/3"
)

# Secuencial (lento)
echo "=== SECUENCIAL ==="
time for url in "${urls[@]}"; do
curl -s -o /dev/null -w "%{url}: %{time_total}s\n" "$url"
done

echo ""

# Paralelo con background jobs
echo "=== PARALELO ==="
time {
for i in "${!urls[@]}"; do
(
result=$(curl -s -o /dev/null -w "%{time_total}" --max-time 10 "${urls[$i]}")
echo "URL $i: ${result}s"
) &
done
wait
}

echo ""

# Paralelo con xargs
echo "=== PARALELO (xargs) ==="
time printf '%s\n' "${urls[@]}" | \
xargs -P 4 -I{} curl -s -o /dev/null -w "{}: %{time_total}s\n" {}

Polling (esperar un estado)

#!/bin/bash
# wait-for-service.sh - Esperar hasta que un servicio esté ready

wait_for_service() {
local url="$1"
local max_wait="${2:-120}" # Segundos máximo de espera
local interval="${3:-5}" # Intervalo entre checks
local elapsed=0

echo "Esperando que $url esté disponible..."

while [[ $elapsed -lt $max_wait ]]; do
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url")

if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
echo "✅ Servicio disponible (HTTP $http_code) después de ${elapsed}s"
return 0
fi

printf " ⏳ [%3ds/%ds] HTTP %s - esperando...\n" "$elapsed" "$max_wait" "$http_code"
sleep "$interval"
elapsed=$((elapsed + interval))
done

echo "❌ Timeout: servicio no disponible después de ${max_wait}s"
return 1
}

# Uso
wait_for_service "https://httpbin.org/get" 60 3

Procesamiento de respuestas con jq

#!/bin/bash
# process-api.sh - Obtener y procesar datos de API

# Obtener datos
data=$(curl -s "https://jsonplaceholder.typicode.com/users")

# Procesar: Tabla de usuarios
echo "USUARIOS:"
echo "$data" | jq -r '.[] | [.id, .name, .email, .company.name] | @tsv' | \
column -t -s $'\t'

echo ""

# Estadísticas
total=$(echo "$data" | jq 'length')
ciudades=$(echo "$data" | jq '[.[].address.city] | unique | length')
empresas=$(echo "$data" | jq '[.[].company.name] | unique')

echo "Total usuarios: $total"
echo "Ciudades únicas: $ciudades"
echo "Empresas: $(echo "$empresas" | jq -r 'join(", ")')"

Configuración desde archivo

#!/bin/bash
# config-driven.sh - Peticiones desde un archivo de configuración

# config.json
cat <<'EOF' > /tmp/api-config.json
{
"endpoints": [
{
"nombre": "API Principal",
"url": "https://httpbin.org/get",
"metodo": "GET",
"headers": {"X-Custom": "valor1"},
"timeout": 10
},
{
"nombre": "Health Check",
"url": "https://httpbin.org/status/200",
"metodo": "GET",
"headers": {},
"timeout": 5
},
{
"nombre": "Crear Recurso",
"url": "https://httpbin.org/post",
"metodo": "POST",
"headers": {"Content-Type": "application/json"},
"body": {"test": true},
"timeout": 10
}
]
}
EOF

# Procesar configuración
jq -c '.endpoints[]' /tmp/api-config.json | while read -r endpoint; do
nombre=$(echo "$endpoint" | jq -r '.nombre')
url=$(echo "$endpoint" | jq -r '.url')
metodo=$(echo "$endpoint" | jq -r '.metodo')
timeout=$(echo "$endpoint" | jq -r '.timeout')

echo "Ejecutando: $nombre ($metodo $url)"

args=(-s -X "$metodo" --max-time "$timeout" -o /dev/null -w "%{http_code}")

# Agregar headers
while IFS='=' read -r key value; do
[[ -n "$key" ]] && args+=(-H "$key: $value")
done < <(echo "$endpoint" | jq -r '.headers | to_entries[] | "\(.key)=\(.value)"')

# Agregar body si existe
body=$(echo "$endpoint" | jq -r '.body // empty')
[[ -n "$body" ]] && args+=(-d "$body")

code=$(curl "${args[@]}" "$url")
printf " → HTTP %s\n\n" "$code"
done

Ejemplo: Monitor de servicios

#!/bin/bash
# service-monitor.sh - Monitoreo continuo

SERVICIOS='[
{"nombre": "API", "url": "https://httpbin.org/get", "esperado": 200},
{"nombre": "Auth", "url": "https://httpbin.org/status/200", "esperado": 200},
{"nombre": "DB Health", "url": "https://httpbin.org/status/200", "esperado": 200}
]'

INTERVALO=10
LOG_FILE="/tmp/monitor.log"

monitor() {
while true; do
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] Verificando servicios..." | tee -a "$LOG_FILE"

echo "$SERVICIOS" | jq -c '.[]' | while read -r svc; do
nombre=$(echo "$svc" | jq -r '.nombre')
url=$(echo "$svc" | jq -r '.url')
esperado=$(echo "$svc" | jq -r '.esperado')

start=$(date +%s%N)
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url")
end=$(date +%s%N)
ms=$(( (end - start) / 1000000 ))

if [[ "$code" -eq "$esperado" ]]; then
printf " ✅ %-15s HTTP %s (%dms)\n" "$nombre" "$code" "$ms" | tee -a "$LOG_FILE"
else
printf " ❌ %-15s HTTP %s (esperado: %s)\n" "$nombre" "$code" "$esperado" | tee -a "$LOG_FILE"
fi
done

echo "" | tee -a "$LOG_FILE"
sleep "$INTERVALO"
done
}

echo "Monitor iniciado (Ctrl+C para detener)"
monitor

Ejercicios

  1. Creá una librería de funciones (lib-api.sh) con funciones para GET, POST, PUT, DELETE
  2. Implementá un script con reintentos exponenciales que pruebe con httpbin.org/status/500
  3. Creá un script que haga 10 peticiones en paralelo y reporte el tiempo total
  4. Implementá polling: esperá a que un endpoint devuelva 200
  5. Creá un monitor de servicios que escriba logs y alerte cuando un servicio falle