A declaração, que vou manter breve, uma vez que quase não se aplica aqui: somos a Geonode e vendemos proxies. Um pedido POST não requer nada da nossa parte. Tudo o que se segue funciona a partir da sua própria ligação, dirigindo-se aos seus próprios pontos finais. Os proxies só entram em cena muito mais tarde, e há uma breve nota no final sobre a única coisa que muda realmente quando se envia um pedido POST através de um proxy — o que não é o que a maioria das pessoas espera.
Noções básicas
curl -d "name=Ada&role=engineer" https://api.example.com/users
O manual do curl descreve a opção -d, --data
: «Envia os dados especificados para um servidor. Para HTTP(S), isto é feito com o método POST, da mesma forma que um navegador faz quando um utilizador preenche um formulário HTML e clica no botão de envio. Esta opção faz com que o curl passe os dados para o servidor utilizando o tipo de conteúdo application/x-www-form-urlencoded
.”
Acontecem duas coisas automaticamente e ambas são importantes.
**-d
implica POST.** Não é necessário utilizar -X POST
, e adicioná-lo não altera nada, exceto no tratamento de redirecionamentos, onde -X
é aplicado a cada salto.
**-d
define Content-Type: application/x-www-form-urlencoded
.** Isto está correto para envios de formulários e errado para quase tudo o resto.
Pode repetir -d
e o curl une as partes: o manual refere que «utilizar -d name=daniel -d skill=lousy
geraria um bloco POST semelhante a name=daniel&skill=lousy
.»
Envio de JSON
A utilização prática mais comum e onde surgem os erros.
A forma explícita:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name":"Ada","role":"engineer"}'
O atalho que a maioria das pessoas ainda não conhece. O curl tem uma opção dedicada «--json», documentada da seguinte forma: «Envia os dados JSON especificados numa solicitação POST para o servidor HTTP. --json funciona como um atalho para passar estas três opções: --data-binary [arg], --header "Content-Type: application/json", --header "Accept: application/json".»
curl --json '{"name":"Ada","role":"engineer"}' https://api.example.com/users
Três opções numa só, e define «Accept», bem como «Content-Type», que é normalmente o que se pretende. Também lê a partir de um ficheiro ou do stdin com «@»:
curl --json @payload.json https://api.example.com/users
cat payload.json | curl --json @- https://api.example.com/users
Uma advertência honesta do manual: «Não há verificação de que os dados passados sejam realmente JSON ou de que a sintaxe esteja correta.» Define cabeçalhos; não valida. Um corpo malformado continua a ser enviado com um tipo de conteúdo JSON, e a reclamação do servidor será sobre o seu JSON e não sobre o curl.
Os cabeçalhos que define «podem ser substituídos com --header, como habitualmente», pelo que pode manter o atalho e ajustar apenas uma parte do mesmo.
As aspas simples são importantes. Coloque os corpos JSON entre aspas simples para que o shell não expanda $ nem interprete as aspas duplas no interior. Se o seu JSON também contiver aspas simples, coloque-o num ficheiro.
As cinco opções de dados e quando utilizar cada uma
Esta é a parte que esclarece a maior parte das dúvidas, uma vez que o curl dispõe de várias variantes de «--data» que diferem em aspetos específicos.
| Opção | Tipo de conteúdo definido | É especial para «@»? | Novas linhas | Utilizar para |
|---|---|---|---|---|
-d / --data | form-urlencoded | Sim, lê um ficheiro | Removidos | Envios de formulários |
--data-raw | form-urlencoded | Não | Removidos | Dados que começam por @ |
--data-binary | form-urlencoded | Sim | Preservados | Ficheiros, bytes exatos |
--data-urlencode | form-urlencoded | Sim | Codificados | Valores com caracteres especiais |
--json | application/json | Sim | Preservados | Corpos JSON |
--data-raw existe por uma razão: o manual indica que envia dados «de forma semelhante a --data, mas sem a interpretação especial do caractere @». Se os seus dados literais começarem por @ — um endereço de e-mail, um nome de utilizador, uma menção — -d tentará lê-los como um nome de ficheiro e falhará, o que pode causar confusão. O exemplo que eles próprios dão é curl --data-raw "@at@at@".
--data-binary é o endereço a utilizar para ficheiros. O manual: «Envie os dados exatamente como especificado, sem qualquer tipo de processamento adicional... as novas linhas e os retornos de carro são preservados e nunca são feitas conversões.» Note-se que, por predefinição, continua a enviar application/x-www-form-urlencoded; por isso, se estiver a enviar dados binários arbitrários, o manual indica que deve substituir essa predefinição: -H "Content-Type: application/octet-stream".
É por isso que -d @file.json pode falhar de forma subtil: as novas linhas são removidas. Para JSON, isso normalmente não importa; para qualquer coisa em que os espaços em branco sejam significativos, importa sim. --data-binary @file.json ou --json @file.json é a forma mais segura.
--data-urlencode lida com valores que contenham &, =, espaços ou qualquer outra coisa que possa comprometer a codificação do formulário. O manual documenta várias sintaxes, e a que se pretende é quase sempre name=content, que codifica o conteúdo como URL e deixa o nome inalterado:
curl --data-urlencode "comment=hello & goodbye = fine" https://example.com/post
Sem isso, o «&» seria interpretado como um separador de campos e o seu comentário seria truncado sem aviso prévio. Existe também o «name@filename», que carrega o conteúdo a partir de um ficheiro, codifica-o para URL e acrescenta «=» ao nome.
Envio de ficheiros e formulários multiparte
Para o envio efetivo de ficheiros, a opção é -F, que funciona de forma diferente de -d.
O manual: «-F, --form <name=content>... simula um formulário preenchido no qual o utilizador clicou no botão de envio. Isto faz com que o curl envie dados POST utilizando o Content-Type multipart/form-data, de acordo com a RFC 2388.»
curl -F "file=@report.pdf" -F "title=Q3 Report" https://api.example.com/upload
Vale a pena compreender a distinção entre @ e <, pois não é intuitiva. O manual: «Para forçar a parte “content” a ser um ficheiro, anteponha ao nome do ficheiro o símbolo @. Para obter a parte “content” a partir de um ficheiro, anteponha ao nome do ficheiro o símbolo <. A diferença entre @ e < reside, portanto, no facto de @ fazer com que um ficheiro seja anexado na publicação como um upload de ficheiro, enquanto < cria um campo de texto e obtém o conteúdo desse campo a partir de um ficheiro.»
Assim, @ carrega um ficheiro como tal; < envia o conteúdo de um ficheiro como valor de um campo de texto.
Para definir um tipo de conteúdo numa parte:
curl -F "file=@data.csv;type=text/csv" https://api.example.com/upload
E se precisar de um valor literal que comece por @ ou <, utilize --form-string, que não interpreta nenhum desses caracteres.
Não defina Content-Type: multipart/form-data manualmente. O curl gera-o incluindo um parâmetro de delimitação, e substituí-lo produz uma solicitação que o servidor não consegue analisar — uma causa muito comum de erros 400 e 415 inexplicáveis.
Para um PUT simples de um ficheiro, -T é mais simples: «Carregar o ficheiro local especificado para o URL remoto... Se esta opção for utilizada com um URL HTTP(S), é utilizado o método PUT.»
Autenticação e cabeçalhos
curl --json '{"a":1}' \
-H "Authorization: Bearer eyJhbG..." \
https://api.example.com/items
-H
é repetível e substitui as predefinições do curl, incluindo as definidas por --json
.
Para autenticação básica, -u user:password
— ou apenas -u user
, o que faz com que o curl solicite a palavra-passe, para que esta não fique registada no histórico do shell. O manual refere que «em sistemas onde funciona, o curl oculta o argumento da opção especificada das listas de processos», acrescentando que «isto não é suficiente para proteger as credenciais».
Para uma API baseada em sessão, capture e reutilize os 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
Depurar um POST que não funciona
Uma sequência curta que resolve quase tudo.
Veja exatamente o que enviou:
curl -v --json '{"a":1}' https://api.example.com/items 2>&1 | grep -E '^[<>]'
As linhas que começam por >
correspondem ao seu pedido, enquanto <
corresponde à resposta. Confirme se o método, o Content-Type
e o corpo da mensagem são os que pretendia. Uma percentagem surpreendente de relatos do tipo «a API não funciona» resolve-se aqui.
Leia o código de estado e o corpo do erro:
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
Interprete as falhas mais comuns:
| Estado | Significa normalmente |
|---|---|
| 400 | Corpo malformado ou campo obrigatório em falta |
| 401 | Credenciais em falta ou inválidas |
| 403 | Autenticado, mas sem permissão |
| 405 | O endpoint não aceita POST — verifique o URL e o método |
| 413 | Corpo demasiado grande |
| 415 | «Content-Type |
» incorreto — o clássico erro «-d | |
| » com JSON | |
| 422 | Tipo de conteúdo correto, mas os dados não passaram na validação |
A distinção entre 415 e 422 é aquela que vale a pena interiorizar: 415 significa que o «wrapper» está errado, 422 significa que o conteúdo está. Abordámos este tema em detalhe em o que é um código de estado 415.
Destaque as falhas nos 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
devolve um código de saída diferente de zero em caso de erros HTTP, ao mesmo tempo que continua a apresentar o corpo da resposta, o que é o desejável quando a API devolve mensagens de erro JSON úteis. O simples --fail
descarta o corpo, o que faz com que a explicação seja perdida.
Envio de POST através de um proxy
Resumidamente, o ponto importante é mais uma advertência do que uma técnica.
curl -x http://user:pass@proxy.example.com:9000 \
--json '{"a":1}' https://api.example.com/items
O funcionamento não sofre alterações. O que muda é o cálculo das tentativas de reenvio, e esta é a parte que vale a pena ter em conta antes de adicionar --retry
.
O POST geralmente não é idempotente. Enviá-lo duas vezes pode criar dois registos. A opção «--retry
» do curl só é ativada em condições transitórias por predefinição, mas «--retry-all-errors
» alarga consideravelmente esse âmbito — e, através de um proxy, um código de erro 5xx significa frequentemente que o destino recusou o pedido, em vez de ter tido um momento de falha. Tentar novamente é, na melhor das hipóteses, inútil e, na pior, duplica uma gravação.
Um tempo de espera não é prova de falha. Se um pedido atingir o tempo de espera depois de o servidor o ter recebido, a operação pode ter sido concluída enquanto se via um erro. Através de um proxy, existe um salto adicional onde isto pode acontecer. Se a operação for importante, utilize uma chave de idempotência — a maioria das APIs sérias suporta uma — em vez de confiar na lógica de repetição para garantir a segurança.
E verifique qual o salto que recusou o seu pedido. %{http_connect}
apresenta a resposta do proxy ao CONNECT separadamente do estado do 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 o proxy solicitou credenciais. connect=200 status=403
significa que o proxy funcionou e o destino recusou. Problemas diferentes, soluções diferentes.
Perguntas frequentes
Como envio um pedido POST com o curl?
curl -d "key=value" URL. A opção -d implica um pedido POST, pelo que -X POST é desnecessário. Além disso, define Content-Type: application/x-www-form-urlencoded, o que é correto para o envio de formulários, mas incorreto para JSON.
Como envio um pedido POST com JSON usando o curl?
curl --json '{"key":"value"}' URL é o atalho — define --data-binary e, além disso, define os cabeçalhos Content-Type e Accept como application/json. A forma mais longa é -X POST -H "Content-Type: application/json" -d '...'. Note que --json não valida o seu JSON.
Por que recebo o erro 415 Unsupported Media Type do curl?
Quase sempre porque utilizou -d com um corpo JSON sem definir o tipo de conteúdo. -d envia dados codificados como formulário, e uma API que espera JSON rejeita-os. Utilize --json ou adicione -H "Content-Type: application/json".
Qual é a diferença entre -d e --data-raw?
-d interpreta um @ inicial como «ler a partir deste ficheiro». --data-raw não o faz, pelo que é o que precisa quando os seus dados literais começam com @ — um endereço de e-mail ou um identificador, por exemplo. Fora isso, comportam-se de forma idêntica.
Como faço para carregar um ficheiro com o POST do curl?
curl -F "file=@document.pdf" URL, que envia multipart/form-data. Utilize @ para anexar o ficheiro como um ficheiro e < para enviar o seu conteúdo como um valor de campo de texto. Não defina manualmente o cabeçalho Content-Type — o curl gera-o com o delimitador necessário.
Como posso enviar dados POST a partir de um ficheiro?
Utilize curl --json @payload.json URL para JSON, ou --data-binary @file para bytes exatos, incluindo novas linhas. Evite -d @file quando os espaços em branco forem importantes, porque -d remove novas linhas e retornos de carro.
Como posso enviar um valor que contenha um símbolo «&» (ampersand) através de um pedido POST?
Utilize --data-urlencode "field=value with & inside". Com o -d simples, o símbolo «&» é interpretado como um separador de campos e o seu valor é truncado silenciosamente nesse ponto.
Devo tentar novamente um pedido POST que falhou?
Com cuidado. O POST geralmente não é idempotente, pelo que uma nova tentativa pode criar uma duplicação — e um tempo de espera esgotado não prova que o servidor não tenha processado o pedido. Utilize uma chave de idempotência sempre que a API a suportar e tenha cuidado com --retry-all-errors, especialmente através de um proxy, onde um código de erro 5xx significa frequentemente uma recusa, em vez de uma falha transitória.
Conclusão
Todo o assunto resume-se a uma pergunta: que tipo de conteúdo é que o endpoint pretende e será que o seu comando o envia?
-d envia dados codificados como «form», o que é adequado para envios de formulários, mas inadequado para JSON — e essa única incompatibilidade está na origem da maioria dos erros 415 com que as pessoas se deparam. --json é a opção a utilizar em vez disso, e é pouco conhecida: um sinalizador que define o tratamento do corpo e ambos os cabeçalhos, com suporte @ para ficheiros e stdin.
Para além disso, existem variantes por razões específicas que vale a pena recordar. --data-raw quando os seus dados começam por @. --data-binary quando as quebras de linha são importantes. --data-urlencode quando um valor contém caracteres que iriam quebrar a codificação do formulário. -F para uploads de ficheiros propriamente ditos, com @ para anexar um ficheiro e < para ler um campo de texto a partir de um ficheiro.
E quando algo falhar, execute-o com -v e leia as linhas > antes de alterar qualquer coisa. A solicitação que enviou não é, frequentemente, a solicitação que pensava ter enviado, e é nessa discrepância que reside a maior parte da confusão nesta área.
