HTTP, pedidos e Ajax

URLs, mensagens, métodos, estados, cookies, cache, promessas, JSON, fetch, SOP e CORS.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Ao pesquisar Redes, o navegador envia /livros?q=redes. O servidor pode devolver uma página HTML ou uma lista JSON. O pedido descreve o recurso e a operação; a resposta descreve o resultado. HTTP não associa por si os pedidos ao mesmo utilizador. Um cookie pode transportar o identificador com que o servidor recupera a sessão.

URL e origem

Em https://biblioteca.example:443/livros/12?q=redes#reserva, temos esquema https, host biblioteca.example, porta 443, caminho /livros/12, query q=redes e fragmento reserva. A porta habitual de HTTPS é 443 e a de HTTP é 80. O fragmento fica no navegador e não integra o alvo enviado ao servidor.

Uma origem combina esquema, host e porta. https://biblioteca.example e http://biblioteca.example são origens distintas. Duas páginas com caminhos diferentes, mas o mesmo esquema, host e porta, têm a mesma origem. URL localiza um recurso; URI é o conceito mais geral de identificação, que também inclui identificadores sem localização de acesso.

Ler mensagens

Esta representação textual usa HTTP/1.1. Versões mais recentes mudam a codificação no transporte, conservando conceitos como método, estado e campos de cabeçalho.

GET /livros?q=redes HTTP/1.1
Host: biblioteca.example
Accept: application/json
Cookie: PHPSESSID=exemplo

A primeira linha identifica método, alvo e versão. Host identifica o servidor pedido; Accept indica formatos que o cliente aceita. A linha vazia separa cabeçalhos do corpo, que aqui não existe.

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 20
Cache-Control: no-store

[{"titulo":"Redes"}]

Content-Type descreve o corpo enviado, não os formatos desejados. Accept é uma preferência do cliente; o servidor pode responder com um formato compatível ou recusar quando não consegue satisfazê-la. Content-Length conta os bytes do corpo, não letras nem linhas. Na rede, HTTP/1.1 separa linhas com CRLF. Um pedido com corpo precisa de enquadramento, como Content-Length ou Transfer-Encoding, para o servidor saber onde termina.

Um formulário normal pode produzir:

POST /reservas HTTP/1.1
Host: biblioteca.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

livro=12&quantidade=2

JSON usa outra codificação, como {"livro":12,"quantidade":2}, e Content-Type: application/json. Em PHP, não confundas essa mensagem com $_POST: lê o corpo e descodifica JSON explicitamente. Uploads costumam usar multipart/form-data e limites que separam as partes.

No vídeo, acompanha uma troca HTTP e localiza a linha vazia que separa cabeçalhos e corpo.

Abrir no YouTube

Métodos, segurança e idempotência

Um método seguro pede leitura, sem solicitar uma alteração de estado da aplicação. Idempotente significa que repetir o mesmo pedido tem o mesmo efeito pretendido que o fazer uma vez. A resposta pode variar e pode haver efeitos acessórios, como registos de acesso.

MétodoUso habitualSeguroIdempotente
GETObter uma representaçãoSimSim
HEADObter cabeçalhos como em GET, sem corpoSimSim
POSTSubmeter dados para processamentoNãoNão, em geral
PUTCriar ou substituir no endereço escolhidoNãoSim
DELETERemover a associação do recursoNãoSim
PATCHAplicar alterações parciaisNãoDepende da operação
OPTIONSConsultar opções de comunicaçãoSimSim

Dois DELETE /reservas/7 podem responder primeiro 204 e depois 404, conservando a idempotência da remoção. Dois POST /reservas podem criar duas reservas. Uma API pode implementar chaves de idempotência para uma operação POST, mas isso é um contrato adicional da aplicação. GET /apagar?id=7 viola a semântica segura, apesar do nome do método.

REST usa recursos, representações e uma interface uniforme. /livros/12 identifica um recurso; o método indica a operação. JSON não torna uma API automaticamente REST. Stateless significa que cada pedido inclui a informação necessária para o interpretar; não proíbe guardar livros ou reservas na base de dados.

O vídeo relaciona recursos e métodos. Compara essa organização com uma API que usa sempre POST /executar, mesmo quando devolve JSON.

Abrir no YouTube

Estados, cookies e cache

EstadoInterpretação
200Pedido bem-sucedido, com representação quando aplicável
201Recurso criado, frequentemente identificado por Location
204Sucesso sem corpo
303Pedir ao cliente que consulte outro destino, normalmente por GET
304A representação em cache continua válida, sem novo corpo
400Pedido inválido
401Faltam credenciais válidas de autenticação
403Operação recusada
404Recurso não encontrado, ou existência não divulgada
405Método não permitido; Allow indica os admitidos
500Erro interno do servidor

Set-Cookie vai na resposta para pedir que o navegador guarde um cookie; Cookie vai em pedidos posteriores conforme domínio, caminho, segurança e política aplicáveis. Um cookie é entrada controlada pelo cliente, não prova independente de permissão. HttpOnly impede leitura por JavaScript; não impede que o navegador envie o cookie em pedidos.

Segue duas trocas. Na primeira, o servidor envia a representação com ETag: "v1". Na segunda, o cliente envia If-None-Match: "v1". Se nada mudou, o servidor responde 304 sem corpo e o cliente reutiliza a cache. Cache-Control: no-store pede que não guardem a resposta. no-cache admite guardá-la, mas exige revalidação antes de a reutilizar. Não guardes respostas privadas em caches partilhadas por engano.

Promessas e ordem de execução

Uma Promise representa um resultado futuro, que pode cumprir-se ou rejeitar-se. then regista o que fazer após cumprir; catch trata rejeições; finally permite finalizar. async faz a função devolver uma promessa; await suspende essa função até à conclusão, sem bloquear todo o navegador.

console.log('A');
Promise.resolve().then(() => console.log('B'));
console.log('C');

A saída é A, C, B, porque o código corrente acaba antes do callback da promessa. Quando a promessa fica cumprida, as reações registadas com then são agendadas como microtarefas e correm antes da tarefa seguinte, como um callback de temporizador entretanto pronto.

A promessa pendente em si não é uma microtarefa; só as reações após cumprimento ou rejeição o são. Um ciclo pesado continua a bloquear os callbacks. async não transforma cálculo síncrono numa thread: apenas permite suspender à espera de operações assíncronas.

Promise.all reúne resultados e rejeita se uma das promessas rejeitar. Só inicia operações concorrentes se elas forem efetivamente iniciadas antes de esperar por cada uma. Erros lançados dentro de async tornam-se rejeições; envolve await num try/catch para os tratar nesse percurso.

JSON e fetch

JSON representa objetos, arrays, strings, números, booleanos e null. Na troca entre sistemas, usa UTF-8; o media type application/json não define um parâmetro charset. Nomes de propriedades e strings usam aspas duplas; não admite comentários, funções nem vírgulas finais. JSON.stringify transforma valores em texto, e JSON.parse faz o inverso. Não uses eval para ler JSON.

Esta função é um excerto para uma página com ul#resultados:

async function pesquisar(termo) {
  const parametros = new URLSearchParams({ q: termo });
  const resposta = await fetch('/api/livros?' + parametros);
  if (!resposta.ok) throw new Error('HTTP ' + resposta.status);
  const livros = await resposta.json();
  const lista = document.querySelector('#resultados');
  lista.replaceChildren();
  for (const livro of livros) {
    const item = document.createElement('li');
    item.textContent = livro.titulo;
    lista.append(item);
  }
}

fetch resolve a promessa mesmo para muitos erros HTTP, incluindo 404 e 500. É o teste a ok, que corresponde a estados 200 a 299, que os transforma em erro neste programa. Erros de rede podem rejeitar antes de existir resposta.

json() lê e descodifica o corpo e também é assíncrono, por isso precisa de await. O cabeçalho do formato não substitui a validação do conteúdo recebido: um 200 com HTML continua a falhar em json().

Ajax atualiza parte da página sem navegar. Enquanto o pedido está pendente, o utilizador pode iniciar outra pesquisa. A ordem de chegada pode ser diferente da ordem de envio:

A resposta mais recente pode chegar primeiroO navegador envia os pedidos 1, Redes, e 2, SQL. A resposta 2 chega primeiro e apresenta SQL. A resposta 1 chega depois e é ignorada: o pedido atual continua a ser 2. O tempo avança de cima para baixo.NavegadorServidor1: Redes2: SQLResposta 2Resposta 1Mostra SQL: atual = 2Ignora Redes: 1 ≠ 2
O tempo avança de cima para baixo. O número identifica o pedido, não a ordem de chegada.

O contador guarda o número do último pedido iniciado. Cada callback conserva o seu número e compara-o com o atual antes de mudar a lista ou a mensagem de erro. No exemplo, 1 !== 2 impede a resposta antiga de apagar SQL.

AbortController pode cancelar o pedido anterior, mas não dispensa o contrato da interface: continua a ser preciso apresentar a pesquisa atual e ignorar resultados que cheguem fora de ordem.

Codificar parâmetros com URLSearchParams conserva espaços, &, + e acentos como dados. Por exemplo, q=redes+%26+web transporta o termo redes & web, sem criar um segundo parâmetro.

Aplicação local completa

Para experimentar a comunicação real, guarda os três ficheiros seguintes na mesma pasta e corre php -S localhost:8000. Abre /ajax.html. O catálogo está em memória para isolar HTTP, não persiste alterações.

ajax.html
<!doctype html>
<html lang="pt-PT">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Pesquisar livros</title>
    <script src="ajax.js" defer></script>
  </head>
  <body>
    <h1>Pesquisar livros</h1>
    <form id="pesquisa">
      <label for="q">Título</label><input id="q" name="q" />
      <button>Pesquisar</button>
    </form>
    <p id="estado" aria-live="polite"></p>
    <ul id="resultados"></ul>
  </body>
</html>
ajax.js
let pedido = 0;
const estado = document.querySelector('#estado');
const lista = document.querySelector('#resultados');
document
  .querySelector('#pesquisa')
  .addEventListener('submit', async (evento) => {
    evento.preventDefault();
    const atual = ++pedido;
    estado.textContent = 'A pesquisar…';
    const parametros = new URLSearchParams({
      q: document.querySelector('#q').value,
    });
    try {
      const resposta = await fetch('api-livros.php?' + parametros);
      if (!resposta.ok) throw new Error('HTTP ' + resposta.status);
      const livros = await resposta.json();
      if (atual !== pedido) return;
      lista.replaceChildren();
      for (const livro of livros) {
        const item = document.createElement('li');
        item.textContent = livro.titulo;
        lista.append(item);
      }
      estado.textContent = `Resultados: ${livros.length}`;
    } catch (erro) {
      if (atual !== pedido) return;
      lista.replaceChildren();
      estado.textContent = 'Pesquisa indisponível.';
    }
  });
api-livros.php
<?php
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    header('Allow: GET');
    http_response_code(405);
    echo json_encode(['erro' => 'Método não permitido']);
    exit;
}
$termo = $_GET['q'] ?? '';
if (!is_string($termo) || strlen($termo) > 100) {
    http_response_code(400);
    echo json_encode(['erro' => 'Pesquisa inválida']);
    exit;
}
$catalogo = [['titulo' => 'Redes'], ['titulo' => 'Algoritmos'], ['titulo' => 'SQL']];
$resultados = array_values(array_filter($catalogo, function ($livro) use ($termo) {
    return stripos($livro['titulo'], $termo) !== false;
}));
echo json_encode($resultados, JSON_UNESCAPED_UNICODE);

Pesquisar red mostra Redes e Resultados: 1; vazio mostra os três livros. As validações no início do endpoint produzem 405 para método diferente de GET e 400 para q[]=red, que chega como array. Separadamente, array_values repõe índices consecutivos após filtrar, para json_encode emitir um array JSON como [{...}] em vez de um objeto.

XMLHttpRequest faz o mesmo tipo de comunicação por callbacks: open configura, setRequestHeader define campos depois de abrir, send envia, e load indica chegada da resposta. Verifica status antes de usar responseText. No callback normal, this pode ser o objeto do pedido; numa arrow function, essa associação não segue a mesma regra.

Para enviar um formulário por Ajax, indica método e corpo. Este excerto pressupõe uma ação /actions/reservar.php que devolve JSON e verifica sessão, token CSRF e dados:

async function reservar(livro, quantidade, csrf) {
  const resposta = await fetch('/actions/reservar.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ livro, quantidade, csrf }),
  });
  if (!resposta.ok) throw new Error('HTTP ' + resposta.status);
  return resposta.json();
}

O corpo codificado chega a $_POST. A função devolve uma promessa, que o chamador deve esperar e tratar com try/catch ou .catch(...). Se o endpoint redirecionar para HTML em vez de devolver JSON, json() falha mesmo que o estado final seja 200. O cliente e o servidor têm de concordar no formato da resposta.

SOP e CORS

A same-origin policy restringe o acesso de scripts a dados de outras origens. Não impede todos os pedidos entre sítios: formulários e recursos embebidos podem causar pedidos sem permitir ler a resposta. Por isso SOP e CORS não substituem proteção contra CSRF: um pedido de alteração pode ser enviado sem que o script consiga ler a resposta, e o servidor precisa na mesma de verificar a intenção.

CORS permite ao servidor declarar quais as origens autorizadas a ler a resposta. Um pedido que exige preflight envia primeiro OPTIONS, com Origin, método e cabeçalhos pretendidos. Não há preflight em todos os pedidos. Um GET sem cabeçalhos adicionais pode ser enviado diretamente; um POST com Content-Type: application/json exige normalmente preflight. O servidor responde com os campos de autorização adequados. Por exemplo, para uma página em https://a.example enviar JSON a https://api.example:

OPTIONS /reservas HTTP/1.1
Host: api.example
Origin: https://a.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://a.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Vary: Origin

Se a resposta autorizar o pedido, o navegador envia o POST. A resposta ao POST também precisa de Access-Control-Allow-Origin, para ser legível pelo script. Vary: Origin informa caches de que a resposta pode depender da origem. A aplicação continua a precisar de autenticar e autorizar o pedido real.

Esta sequência mostra o caso autorizado. Se o preflight falhar, o navegador não envia o POST; se faltar autorização na resposta final, o script não a pode ler.

Para credenciais entre origens, o cliente usa credentials: 'include' e o servidor precisa de permitir credenciais e a origem concreta, não Access-Control-Allow-Origin: *. As regras dos cookies ainda podem impedir o envio. mode: 'no-cors' não desbloqueia leitura de JSON: produz uma resposta opaca.

MPA, SPA e PWA

Uma MPA navega entre documentos produzidos pelo servidor. Uma SPA carrega um documento inicial e atualiza a interface e a navegação sobretudo no cliente. SPA não significa ausência de servidor: continua a precisar de URLs, histórico, estados de carregamento e controlo de acesso. Também não dispensa navegação normal; uma MPA pode usar Ajax em algumas ações sem ser uma SPA.

Uma PWA pode acrescentar manifest, instalação e service worker para cache e funcionamento offline. Esses mecanismos não são sinónimos de SPA. localStorage guarda pares de strings por origem e normalmente conserva-os entre sessões; sessionStorage separa-os também por separador e sessão da página. Nenhum dos dois segue automaticamente em pedidos, ao contrário dos cookies, e nenhum deve guardar segredos acessíveis a qualquer script dessa origem.

A History API pode atualizar o URL e o histórico de uma SPA sem carregar outro documento; o programa precisa de restaurar o conteúdo correto ao voltar atrás. APIs como geolocalização e notificações têm permissões, contextos e limites próprios. Frameworks organizam código e componentes, mas não alteram as regras de HTTP, DOM ou segurança já estudadas.

SSR constrói a representação HTML no servidor; CSR constrói-a sobretudo no cliente a partir de dados. Esta distinção é independente de MPA/SPA: uma MPA pode usar SSR e uma SPA pode pré-renderizar partes no servidor. Um Worker executa trabalho sem acesso direto ao DOM e comunica por mensagens. Um service worker tem um ciclo de instalação e ativação e pode intercetar pedidos das páginas que controla; não é uma função que corre dentro de cada evento do DOM.

Exercícios

Interpretar um pedido

Um pedido é GET /livros/7?modo=curto HTTP/1.1, com Host: catalogo.example e Accept: application/json. O URL original acaba em #preco. Explica cada parte e indica o cabeçalho que deve descrever um corpo JSON da resposta.

Pista

Método e alvo estão na primeira linha; fragmento não integra o alvo.

Mais uma pista

Accept é preferência. Content-Type descreve o corpo efetivo.

Solução

GET pede a representação de /livros/7 com query modo=curto. Host identifica catalogo.example. O fragmento #preco não viaja ao servidor. Accept pede JSON; a resposta JSON deve usar Content-Type: application/json. A resposta ainda inclui uma linha de estado, como 200.

Confere a tua resposta:

  • Separei alvo e fragmento.
  • Distingui Accept e Content-Type.

Erros frequentes

Não responder que Accept descreve o corpo recebido nem anexar #preco à query.

Voltar à explicação
Repetir DELETE e POST

DELETE /reservas/4 responde 204 na primeira chamada e 404 na segunda. Isso viola idempotência? Dois POST /reservas criam duas reservas iguais. Explica a diferença e escolhe o método de uma pesquisa só de leitura.

Pista

Idempotência diz respeito ao efeito pretendido, não à resposta.

Solução

As respostas diferentes não violam idempotência: repetir a remoção conserva o estado final pretendido. POST pode criar de novo, por isso não é idempotente em geral. Uma pesquisa só de leitura usa GET e não deve alterar reservas. Um contrato adicional poderia deduplicar POST, mas não se presume.

Confere a tua resposta:

  • Expliquei DELETE idempotente com estados diferentes.
  • Distingui criação POST de pesquisa GET.

Erros frequentes

Não usar GET para apagar nem confundir igualdade das respostas com idempotência.

Voltar à explicação
Prever callbacks e erros

Prevê a ordem de console.log neste programa. Depois explica por que fetch para um endpoint que responde 404 pode entrar no then em vez do catch.

console.log('um');
Promise.resolve().then(() => console.log('dois'));
console.log('tres');

Pista

O callback da promessa não interrompe o código corrente.

Mais uma pista

Um estado HTTP não bem-sucedido não é automaticamente rejeição de fetch.

Solução

A ordem é um, tres, dois. O código corrente escreve um e tres. A promessa resolvida agenda o callback que escreve dois para depois. No caso de fetch, a promessa resolve mesmo com 404, porque existe uma resposta HTTP. O programa precisa de testar resposta.ok e lançar um erro para o tratar no catch. Erros de rede rejeitam antes de existir resposta.

Confere a tua resposta:

  • Obtive um, tres, dois.
  • Incluí teste a ok para 404.

Erros frequentes

Não esperar que await/fetch bloqueiem toda a página, nem confiar no catch para reconhecer sozinho todos os estados HTTP.

Voltar à explicação
Ler uma API de outra origem

A página https://a.example faz fetch para https://api.example/livros, com GET e sem cabeçalhos adicionais, e precisa de enviar o cookie de sessão da API. Indica a opção do cliente e os cabeçalhos da resposta necessários para ler JSON. Assume que a política do cookie permite o envio. Há preflight por causa dessa opção? mode: "no-cors" resolveria a leitura?

Pista

Credenciais exigem configuração do cliente e permissão da origem concreta.

Mais uma pista

Enviar credenciais não altera, por si, o método nem acrescenta um cabeçalho que exija preflight.

Solução

O cliente usa credentials: "include". A resposta inclui Access-Control-Allow-Origin: https://a.example e Access-Control-Allow-Credentials: true. A origem não pode ser * com credenciais. Neste GET, não há preflight apenas por enviar cookies; a política dos cookies continua a aplicar-se, como assume o enunciado. mode: "no-cors" dá uma resposta opaca, cujo corpo o script não pode ler como JSON.

Confere a tua resposta:

  • Indiquei os campos de permissão e a opção do cliente.
  • Expliquei por que a resposta opaca não permite ler JSON.

Erros frequentes

CORS não autentica o utilizador. Enviar um pedido e ler a resposta são permissões diferentes.

Voltar à explicação
Autorizar o pedido e a leitura

Uma página em https://a.example faz POST com JSON a https://api.example/reservas, sem cookies nem outros cabeçalhos além de Content-Type. O preflight permite essa origem, POST e Content-Type. O POST devolve 200 e JSON, mas não inclui Access-Control-Allow-Origin. O script consegue ler o corpo? Indica os campos pedidos no preflight e a correção.

Pista

O preflight autoriza enviar o pedido; a resposta real tem as suas próprias regras de leitura.

Mais uma pista

A permissão recebida em OPTIONS não acrescenta campos à resposta do POST.

Solução

O navegador envia OPTIONS com Origin: https://a.example, Access-Control-Request-Method: POST e Access-Control-Request-Headers: content-type. Depois da autorização, o POST pode chegar ao servidor e alterar dados. Sem Access-Control-Allow-Origin: https://a.example na resposta real, fetch falha para o script por CORS, mesmo com 200 no servidor. Acrescenta esse campo e Vary: Origin se a resposta variar com a origem. Como não há credenciais, não é necessário permitir credenciais neste caso.

Confere a tua resposta:

  • Identifiquei OPTIONS, origem, método e cabeçalho pretendidos.
  • Exigi Access-Control-Allow-Origin também na resposta real.

Erros frequentes

Não inferir que um erro de CORS anulou uma alteração já feita no servidor.

Voltar à explicação
Conservar a pesquisa mais recente

A pesquisa 1 procura Redes. Antes de terminar, a pesquisa 2 procura SQL. A resposta 2 chega primeiro e apresenta SQL; a resposta 1 chega depois. Que deve ficar visível? Descreve um contador de pedidos que garanta esse resultado e indica o que fazer se, em vez de responder com livros, o pedido 1 falhar depois do sucesso do pedido 2.

Pista
Distingue o número guardado pelo callback do número do último pedido iniciado.
Solução

SQL deve continuar visível. Incrementa um contador em cada submissão e guarda o novo valor numa variável local do callback. Antes de atualizar a lista, compara o valor local com o contador atual. Só o pedido 2 tem os números iguais. O pedido 1 deve sair sem alterar a página. Aplica a mesma condição no catch, antes de limpar a lista ou mostrar um erro: uma falha antiga também não deve substituir o resultado atual. Se a ordem de chegada se inverter, a resposta 1 já é antiga desde que a pesquisa 2 começou, mesmo que esta ainda esteja pendente.

Confere a tua resposta:

  • Apresentei SQL e ignorei a resposta antiga, incluindo o seu erro.
  • Coloquei a verificação antes de alterar lista e mensagem.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.