HTTP, pedidos e Ajax
URLs, mensagens, métodos, estados, cookies, cache, promessas, JSON, fetch, SOP e CORS.
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.
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étodo | Uso habitual | Seguro | Idempotente |
|---|---|---|---|
GET | Obter uma representação | Sim | Sim |
HEAD | Obter cabeçalhos como em GET, sem corpo | Sim | Sim |
POST | Submeter dados para processamento | Não | Não, em geral |
PUT | Criar ou substituir no endereço escolhido | Não | Sim |
DELETE | Remover a associação do recurso | Não | Sim |
PATCH | Aplicar alterações parciais | Não | Depende da operação |
OPTIONS | Consultar opções de comunicação | Sim | Sim |
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.
Estados, cookies e cache
| Estado | Interpretação |
|---|---|
| 200 | Pedido bem-sucedido, com representação quando aplicável |
| 201 | Recurso criado, frequentemente identificado por Location |
| 204 | Sucesso sem corpo |
| 303 | Pedir ao cliente que consulte outro destino, normalmente por GET |
| 304 | A representação em cache continua válida, sem novo corpo |
| 400 | Pedido inválido |
| 401 | Faltam credenciais válidas de autenticação |
| 403 | Operação recusada |
| 404 | Recurso não encontrado, ou existência não divulgada |
| 405 | Método não permitido; Allow indica os admitidos |
| 500 | Erro 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:
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.
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.
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.
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.
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.
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
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.

