La información, que seré breve ya que apenas viene al caso: somos Geonode y vendemos proxies. Una solicitud POST no requiere nada por nuestra parte. Todo lo que se explica a continuación funciona desde tu propia conexión hacia tus propios puntos finales. Los proxies solo entran en escena mucho más adelante, y hay una breve nota al final sobre lo único que realmente cambia cuando envías una solicitud POST a través de uno de ellos —que no es lo que la mayoría de la gente espera—.
Conceptos básicos
curl -d "name=Ada&role=engineer" https://api.example.com/users
El manual de curl describe -d, --data
: «Envía los datos especificados a un servidor. Para HTTP(S), esto se realiza con el método POST, del mismo modo que lo hace un navegador cuando un usuario ha rellenado un formulario HTML y pulsa el botón de envío. Esta opción hace que curl pase los datos al servidor utilizando el tipo de contenido application/x-www-form-urlencoded
».
Hay dos cosas que ocurren automáticamente y ambas son importantes.
**-d
implica POST.** No es necesario utilizar -X POST
, y añadirlo no cambia nada, salvo en la gestión de redireccionamientos, donde se aplica -X
en cada salto.
**-d
establece Content-Type: application/x-www-form-urlencoded
.** Esto es correcto para el envío de formularios, pero incorrecto para casi todo lo demás.
Puedes repetir -d
y curl unirá las partes: el manual señala que «utilizar -d name=daniel -d skill=lousy
generaría un fragmento POST que se vería así: name=daniel&skill=lousy
».
Envío de JSON
El uso práctico más habitual, y donde suelen producirse los errores.
La forma explícita:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name":"Ada","role":"engineer"}'
El atajo que la mayoría de la gente no conoce. curl dispone de una opción específica «--json», documentada de la siguiente manera: «Envía los datos JSON especificados en una solicitud POST al servidor HTTP. --json funciona como un atajo para pasar estas tres opciones: --data-binary [arg], --header "Content-Type: application/json", --header "Accept: application/json"».
curl --json '{"name":"Ada","role":"engineer"}' https://api.example.com/users
Tres opciones en una, y establece tanto Accept como Content-Type, que suele ser lo que se desea. También lee desde un archivo o desde la entrada estándar (stdin) con @:
curl --json @payload.json https://api.example.com/users
cat payload.json | curl --json @- https://api.example.com/users
Una advertencia sincera del manual: «No se verifica que los datos pasados sean realmente JSON ni que la sintaxis sea correcta». Establece los encabezados; no los valida. Un cuerpo mal formado se sigue enviando con un tipo de contenido JSON, y la queja del servidor se referirá a tu JSON en lugar de a curl.
Los encabezados que establece «se pueden anular con --header como de costumbre», por lo que puedes mantener el atajo y ajustar una parte del mismo.
Las comillas simples son importantes. Encerra los cuerpos JSON entre comillas simples para que el shell no expanda $ ni interprete las comillas dobles que haya en su interior. Si tu JSON también contiene comillas simples, guárdalo en un archivo.
Las cinco opciones de datos y cuándo utilizar cada una
Esta es la parte que aclara la mayor parte de las dudas, ya que curl cuenta con varias variantes de «--data» que difieren en aspectos concretos.
| Opción | Tipo de contenido establecido | ¿Es especial «@»? | Saltos de línea | Se utiliza para |
|---|---|---|---|---|
-d / --data | form-urlencoded | Sí, lee un archivo | Eliminados | Envíos de formularios |
--data-raw | form-urlencoded | No | Eliminados | Datos que empiezan por @ |
--data-binary | form-urlencoded | Sí | Conservados | Archivos, bytes exactos |
--data-urlencode | form-urlencoded | Sí | Codificados | Valores con caracteres especiales |
--json | application/json | Sí | Conservados | Cuerpos JSON |
--data-raw existe por una razón: el manual indica que envía datos «de forma similar a --data, pero sin la interpretación especial del carácter @». Si tus datos literales comienzan por @ —una dirección de correo electrónico, un nombre de usuario, una mención—, -d intentará interpretarlos como un nombre de archivo y fallará, lo que puede resultar confuso. Su propio ejemplo es curl --data-raw "@at@at@".
--data-binary es el que hay que utilizar para los archivos. El manual: «Envía los datos exactamente tal y como se especifican, sin ningún tipo de procesamiento adicional... Se conservan los saltos de línea y los retornos de carro, y nunca se realizan conversiones». Ten en cuenta que, por defecto, sigue enviando application/x-www-form-urlencoded, por lo que, si vas a enviar datos binarios arbitrarios, el manual te indica que lo anules: -H "Content-Type: application/octet-stream".
Por eso -d @file.json puede fallar sutilmente: se eliminan los saltos de línea. En el caso del JSON, esto no suele importar; sin embargo, sí que importa en cualquier caso en el que los espacios en blanco sean significativos. --data-binary @file.json o --json @file.json son las formas más seguras.
--data-urlencode gestiona valores que contienen &, =, espacios o cualquier otro elemento que pudiera alterar la codificación de los formularios. El manual documenta varias sintaxis, y la que te interesa es casi siempre name=content, que codifica el contenido según el formato URL y deja el nombre tal cual:
curl --data-urlencode "comment=hello & goodbye = fine" https://example.com/post
Sin ello, ese «&» se interpretaría como un separador de campos y tu comentario se truncaría sin previo aviso. También existe «name@filename», que carga el contenido desde un archivo, lo codifica como URL y añade «=» al nombre.
Carga de archivos y formularios multiparte
Para la carga real de archivos, la opción es -F, y funciona de forma diferente a -d.
El manual indica: «-F, --form <name=content>... emula un formulario rellenado en el que el usuario ha pulsado el botón de envío. Esto hace que curl envíe los datos POST utilizando el tipo de contenido multipart/form-data, de acuerdo con la RFC 2388».
curl -F "file=@report.pdf" -F "title=Q3 Report" https://api.example.com/upload
Merece la pena conocer la distinción entre «@» y «<», ya que no resulta intuitiva. El manual indica: «Para forzar que la parte “content” sea un archivo, antepone al nombre del archivo el símbolo @. Para obtener la parte “content” de un archivo, antepone al nombre del archivo el símbolo <. La diferencia entre @ y < radica, pues, en que @ hace que un archivo se adjunte a la publicación como una carga de archivo, mientras que < crea un campo de texto y obtiene el contenido de dicho campo a partir de un archivo».
Así pues, @ carga un archivo como tal; < envía el contenido de un archivo como valor de un campo de texto.
Para establecer un tipo de contenido en una parte:
curl -F "file=@data.csv;type=text/csv" https://api.example.com/upload
Y si necesitas un valor literal que comience por @ o <, utiliza --form-string, que no interpreta ninguno de esos caracteres.
No establezcas tú mismo Content-Type: multipart/form-data. curl lo genera incluyendo un parámetro de delimitación, y sobrescribirlo genera una solicitud que el servidor no puede analizar —una causa muy común de errores 400 y 415 inexplicables—.
Para una solicitud PUT sencilla de un archivo, -T resulta más sencillo: «Sube el archivo local especificado a la URL remota... Si esta opción se utiliza con una URL HTTP(S), se emplea el método PUT».
Autenticación y encabezados
curl --json '{"a":1}' \
-H "Authorization: Bearer eyJhbG..." \
https://api.example.com/items
-H
es una opción repetible y anula los valores predeterminados de curl, incluidos los que establece --json
.
Para la autenticación básica, -u user:password
— o simplemente -u user
, lo que hace que curl solicite la contraseña para que esta no quede registrada en el historial de tu shell. El manual señala que «en los sistemas en los que funciona, curl oculta el argumento de la opción indicada de los listados de procesos», aunque añade que «esto no es suficiente para proteger las credenciales».
Para una API basada en sesiones, captura y reutiliza las cookies:
curl -c jar.txt -d "user=ada&pass=secret" https://example.com/login
curl -b jar.txt --json '{"a":1}' https://example.com/api/items
Depuración de una solicitud POST que no funciona
Una breve secuencia que resuelve casi todo.
Comprueba exactamente lo que has enviado:
curl -v --json '{"a":1}' https://api.example.com/items 2>&1 | grep -E '^[<>]'
Las líneas que empiezan por >
son tu solicitud, y las que empiezan por <
son la respuesta. Comprueba que el método, el Content-Type
y el cuerpo sean los que pretendías. Una proporción sorprendente de los informes de «la API no funciona» se resuelven aquí.
Lee el código de estado y el cuerpo del error:
curl -sS -o body.txt -D headers.txt --json '{"a":1}' https://api.example.com/items
head -1 headers.txt; head -c 300 body.txt
Interpreta los errores más comunes:
| Estado | Suele significar |
|---|---|
| 400 | Cuerpo mal formado o falta un campo obligatorio |
| 401 | Credenciales ausentes o no válidas |
| 403 | Autenticado, pero sin permiso |
| 405 | El punto final no acepta POST: comprueba la URL y el método |
| 413 | Cuerpo demasiado grande |
| 415 | «Content-Type |
» incorrecto — el clásico error «-d | |
| » con JSON | |
| 422 | El tipo de contenido es correcto, pero los datos no han superado la validación |
La distinción entre 415 y 422 es la que vale la pena interiorizar: 415 significa que el contenedor es incorrecto, mientras que 422 significa que el contenido lo es. Lo hemos tratado en detalle en ¿Qué es un código de estado 415?.
Haz que los errores se noten en los scripts:
curl --fail-with-body --silent --show-error \
--connect-timeout 5 --max-time 30 \
--json @payload.json https://api.example.com/items
--fail-with-body
devuelve un código de salida distinto de cero en caso de errores HTTP, al tiempo que sigue mostrando el cuerpo de la respuesta, que es lo que te interesa cuando la API devuelve mensajes de error JSON útiles. El código simple --fail
descarta el cuerpo, lo que hace que se pierda la explicación.
Envío de POST a través de un proxy
Breve: lo importante es una advertencia más que una técnica.
curl -x http://user:pass@proxy.example.com:9000 \
--json '{"a":1}' https://api.example.com/items
El funcionamiento no cambia. Lo que cambia es el cálculo de los reintentos, y esta es la parte en la que vale la pena pensar antes de añadir --retry
.
Por lo general, el método POST no es idempotente. Enviarlo dos veces puede crear dos registros. La opción «--retry
» de curl solo se activa en condiciones transitorias por defecto, pero «--retry-all-errors
» amplía considerablemente ese ámbito; y, a través de un proxy, un error 5xx suele significar que el destino te ha rechazado, en lugar de que haya tenido un momento de fallo. Reintentarlo es, en el mejor de los casos, inútil y, en el peor, duplica una escritura.
Un tiempo de espera agotado no es prueba de fallo. Si una solicitud agota el tiempo de espera después de que el servidor la haya recibido, es posible que la operación se haya completado mientras veías un error. A través de un proxy hay un salto adicional en el que esto puede ocurrir. Si la operación es importante, utiliza una clave de idempotencia —la mayoría de las API serias la admiten— en lugar de confiar en la lógica de reintentos para estar seguro.
Y comprueba en qué salto se te ha denegado el acceso. %{http_connect}
muestra la respuesta del proxy a la solicitud CONNECT por separado del estado del destino:
curl -sS -o /dev/null -x "$PROXY" \
-w 'connect=%{http_connect} status=%{response_code}\n' \
--json '{"a":1}' https://api.example.com/items
connect=407
significa que el proxy solicitaba credenciales. connect=200 status=403
significa que el proxy funcionó correctamente y que el destino denegó el acceso. Problemas distintos, soluciones distintas.
Preguntas frecuentes
¿Cómo envío una solicitud POST con curl?
curl -d "key=value" URL. La opción -d implica POST, por lo que -X POST es innecesario. Además, establece Content-Type: application/x-www-form-urlencoded, lo cual es correcto para el envío de formularios, pero incorrecto para JSON.
¿Cómo envío JSON mediante POST con curl?
curl --json '{"key":"value"}' URL es el atajo: establece --data-binary y, además, los encabezados Content-Type y Accept en application/json. La forma más larga es -X POST -H "Content-Type: application/json" -d '...'. Ten en cuenta que --json no valida tu JSON.
¿Por qué me aparece el error 415 «Unsupported Media Type» en curl?
Casi siempre se debe a que has utilizado -d con un cuerpo JSON sin establecer el tipo de contenido. -d envía datos codificados como formulario, y una API que espera JSON lo rechaza. Utiliza --json o añade -H "Content-Type: application/json".
¿Cuál es la diferencia entre -d y --data-raw?
-d interpreta un @ al principio como «leer desde este archivo». --data-raw no lo hace, por lo que es lo que necesitas cuando tus datos literales comienzan por @ —una dirección de correo electrónico o un identificador, por ejemplo—. Por lo demás, se comportan de forma idéntica.
¿Cómo subo un archivo con POST de curl?
curl -F "file=@document.pdf" URL, que envía multipart/form-data. Utiliza @ para adjuntar el archivo como tal y < para enviar su contenido como valor de un campo de texto. No establezcas tú mismo el encabezado Content-Type: curl lo genera con el delimitador necesario.
¿Cómo envío datos POST desde un archivo?
Utiliza curl --json @payload.json URL para JSON, o --data-binary @file para los bytes exactos, incluidos los saltos de línea. Evita -d @file cuando los espacios en blanco sean importantes, ya que -d elimina los saltos de línea y los retornos de carro.
¿Cómo envío mediante POST un valor que contiene un símbolo «&»?
Utiliza --data-urlencode "field=value with & inside". Con -d sin formato, el símbolo «&» se interpreta como un separador de campos y tu valor se trunca silenciosamente en ese punto.
¿Debo reintentar un POST que ha fallado?
Con precaución. Por lo general, POST no es idempotente, por lo que un nuevo intento podría generar un duplicado; además, un tiempo de espera agotado no garantiza que el servidor no haya procesado la solicitud. Utiliza una clave de idempotencia cuando la API la admita y ten cuidado con --retry-all-errors, especialmente a través de un proxy, donde un código 5xx suele significar un rechazo más que un fallo transitorio.
Conclusión
Todo el tema se reduce a una pregunta: ¿qué tipo de contenido espera el punto final y lo envía tu comando?
-d envía datos codificados como formulario, lo cual es adecuado para el envío de formularios pero incorrecto para JSON, y esa única discrepancia es la causa de la mayoría de los errores 415 que se producen. --json es la opción que se debe utilizar en su lugar, y es poco conocida: un indicador que configura el manejo del cuerpo y ambos encabezados, con soporte @ para archivos y stdin.
Más allá de eso, existen variantes por razones específicas que conviene recordar. --data-raw cuando tus datos empiezan por @. --data-binary cuando los saltos de línea son importantes. --data-urlencode cuando un valor contiene caracteres que romperían la codificación del formulario. -F para subidas de archivos reales, con @ para adjuntar un archivo y < para leer un campo de texto desde uno.
Y cuando algo falle, ejecútalo con -v y lee las líneas > antes de cambiar nada. La solicitud que has enviado no suele ser la que creías haber enviado, y esa diferencia es la fuente de la mayor parte de la confusión en este ámbito.
