Um formulário AJAX espera JSON mas recebe HTML: percorra a resposta passo a passo
Uma resposta 200 não prova que seja JSON. Percorra o painel Rede passo a passo e separe falhas de transporte, de representação e de contrato.
O formulário é enviado, o indicador aparece e o console diz que há um token < inesperado em JSON na posição 0. A própria página carrega bem, e é por isso que o primeiro impulso é culpar o analisador ou o túnel. Normalmente nenhum dos dois está envolvido: o envio é uma requisição diferente do carregamento da página, e voltou com algo que não é JSON. Trabalhe na ordem em que o navegador realmente revela isso, porque as quatro respostas possíveis exigem quatro correções diferentes.
Passo 1: capture o envio que realmente aconteceu
Abra o painel Rede antes de enviar, deixe o registro configurado para preservar entradas entre navegações e faça um envio de teste inofensivo numa conta que você tenha permissão de usar. Selecione a requisição que o próprio formulário iniciou, não a requisição de documento da página.
Cuidado com filtros. Um filtro XHR ou Fetch é prático, mas se o framework ainda envia por um iframe oculto ou por um formulário criado dinamicamente, o filtro esconderá a única requisição que importa. Envie primeiro uma vez sem filtro e veja o que apareceu.
Para a requisição escolhida, anote seis coisas antes de interpretar qualquer coisa: o método, a URL completa com a string de consulta, o status, a cadeia de redirecionamentos, o Content-Type da resposta e as primeiras linhas do corpo. A referência do painel Rede do Chrome DevTools cobre as visões Headers, Response e Initiator onde isso fica, e os painéis equivalentes de outros navegadores mostram os mesmos campos.
Passo 2: confirme que é a requisição que você pensa
Uma quantidade surpreendente de erros de JSON são, na verdade, erros de roteamento. Se o manipulador do envio lançar uma exceção antes de chamar preventDefault, o navegador faz um POST nativo do formulário para o endereço do atributo action, e o servidor responde com uma página HTML completa. O manipulador nunca executa, logo nunca se pediu JSON.
Compare a URL do painel com a URL que o seu JavaScript chama. Procure um redirecionamento que termine em outro lugar, uma barra final faltando ou sobrando, uma divergência de método em que o manipulador envia GET e o endpoint aceita POST, ou um formulário que envia para um caminho relativo a partir de uma página que mudou de lugar. Siga a cadeia de redirecionamentos até o fim e anote a URL final, não a primeira.
Depois verifique o iniciador. Se a requisição veio de uma navegação de documento e não do seu script, você já encontrou o problema, e nenhuma mudança no analisador de resposta vai ajudar.
Passo 3: classifique o resultado em quatro categorias
Cada resultado pertence exatamente a uma destas, e a categoria decide onde olhar em seguida.
| What you see | Category | Where to look |
|---|---|---|
| Request never finishes, TLS or connection error | No usable response | Endpoint reachability, certificate, front-end timeout |
| Request completes, script gets an opaque failure | Access blocked | Cross-origin policy, credentials mode, missing response header |
| 200 with an HTML body or a login form | Wrong representation | Redirect, expired session, route fallback, error handler |
| Body begins with a notice then valid JSON | Wrong representation | Server-side warning output before serialisation |
| Valid JSON, framework rejects it | Contract mismatch | Envelope shape, field names, value types |
As linhas do meio são as que as pessoas diagnosticam mal, porque o status é 200. Uma página de login alcançada após um redirecionamento é uma resposta HTTP perfeitamente boa e um documento JSON muito ruim. O tipo de conteúdo application/json não conserta isso: é um rótulo, e um corpo mal rotulado continua sem poder ser analisado. Acesso bloqueado é a linha que parece uma requisição concluída: o painel mostra um status, mas a política de origem cruzada retém o corpo, então para o script que chama ela é tão inútil quanto um tempo esgotado.
A quarta linha é igualmente comum em pilhas antigas de PHP e de frameworks. Um aviso de obsolescência impresso antes da carga codificada produz um corpo que nenhum analisador JSON aceitará, e muitas vezes esse aviso é toda a explicação.
Passo 4: leia o erro do analisador ao pé da letra
O texto do erro nomeia o primeiro caractere que quebrou a análise, e isso costuma identificar a categoria sem nenhum palpite.
| Error text (Chrome/V8) | What it means |
|---|---|
| Unexpected token < in JSON at position 0 | The body starts with markup: an HTML page or an error document |
| Unexpected end of JSON input | Empty body, or the response was truncated |
| Unexpected token o in JSON at position 1 | You parsed something already parsed, typically an object rather than text |
| Unexpected token u | The body starts with the bare word undefined, usually an empty response rendered as a string |
Um corpo vazio com 200 é um caso próprio: o endpoint executou, não produziu nada e algo mais adiante tratou isso como sucesso. É uma falha de contrato da aplicação e não de transporte, e o lugar dela é o passo seguinte, não o proxy. Firefox e Safari formulam as mesmas quatro falhas de outra maneira, então compare pelo caractere ou pelo corpo vazio, e não pela string exata.
Passo 5: corrija a camada que as evidências apontam
Para um problema de roteamento, corrija a requisição: ajuste o action, impeça o manipulador de cair, alinhe o endpoint e o método, e teste de novo. Para um problema de representação, corrija o que produziu o corpo e descubra por que um documento de erro volta onde se esperavam dados. Para um problema de contrato, leia a estrutura realmente esperada pelo framework para a versão que você executa e molde a resposta conforme ela, em vez de afrouxar o cliente.
Uma resposta com acesso bloqueado precisa da sua própria passagem, porque a requisição em si pode estar correta e o corpo pode nunca chegar ao seu código. Compare o Access-Control-Allow-Origin da resposta com o Origin exato que o navegador enviou, e lembre que um curinga não satisfaz uma requisição com credenciais. Emparelhe credentials: 'include' no cliente com Access-Control-Allow-Credentials: true no servidor, e verifique se a chamada usou o modo no-cors, em que um corpo opaco é o resultado documentado e não um defeito.
Substituir o analisador por algo permissivo é a única correção que se deve evitar. Avaliar a resposta como código, ou descascar texto até que ela seja analisada, esconde a divergência de contrato e acrescenta uma superfície de injeção em troca de silêncio. Da mesma forma, verificar o status separadamente do corpo é obrigatório: um 200 que carrega um objeto de erro no nível da aplicação é normal em muitas API e deve ser tratado lendo a carga.
Quando você precisar de ajuda de outra pessoa, compartilhe um registro mínimo — método, URL, status, URL final após os redirecionamentos, tipo de conteúdo da resposta, forma do corpo e o contrato esperado — em vez de uma exportação crua. Os arquivos de rede contêm cookies, tokens, strings de consulta e os dados pessoais que estavam no formulário, e um saneador automático não é uma revisão.
- Capture a requisição sem filtro antes de mudar qualquer código.
- Confirme que o iniciador foi o seu script e que a URL final é o seu endpoint.
- Classifique a resposta antes de tocar no analisador.
- Corrija roteamento, acesso, representação ou contrato, e nunca afrouxando o analisador.