AJAX e fetch - applicazioni Web 2.0 e MVC rivisto
In questa pagina 7
Con il Web 1.0 (JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →) ogni azione dell'utente, anche cancellare una riga, produceva una nuova pagina HTML completa: il browser ricaricava tutto, perdeva la posizione di scorrimento e lo stato dei campi. Il Web 2.0 carica la pagina una volta e poi fa lavorare JavaScript: una richiesta HTTP in background, e il DOM modificato soltanto nella parte che cambia. La tecnica si chiama AJAX (Asynchronous JavaScript and XML, nome nato a metà degli anni 2000 quando i dati viaggiavano in XML; oggi si usa JSON). Il server, come in Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →, offre solo dati. La nota presuppone JavaScript - linguaggio, DOM ed eventiJavaScript (standard ECMAScript, Ecma-262) è il linguaggio dei browser: tipizzato dinamicamente, con sette tipi primitivi (number, string, boolean, undefined, null, bigint, symbol) e gli oggetti (anche array e funzioni); typeof null vale "object" per un errore storico; == converte i tipi, === no (si usa sempre ===); valori falsy: false, 0, 0n, "", null, undefined, NaN. let e const hanno visibilità di blocco e una zona morta prima della dichiarazione, var visibilità di funzione e hoisting; const impedisce di riassegnare il nome, non di modificare l'oggetto. Funzioni: dichiarazione (hoisted), espressione, arrow function (this lessicale), parametri predefiniti, rest e spread, destructuring. Array: map, filter, reduce, find, some, every, sort (senza comparatore ordina come stringhe). Closure: una funzione ricorda le variabili dell'ambiente in cui è nata. Eventi: addEventListener; propagazione in tre fasi (cattura, target, bolla), stopPropagation, preventDefault, delegazione con event.target e closest. DOM: albero di nodi; querySelector, createElement, append, replaceChildren, textContent (sicuro) contro innerHTML (rischio di XSS), classList, dataset. Moduli ES (type="module", import e export) sono differiti e in modalità strict. Event loop: un solo thread, call stack, coda dei task (setTimeout, eventi) e coda dei microtask (promise, await) svuotata dopo ogni task: l'esecuzione asincrona (fetch, async e await, Promise.all) è alla base di AJAX e il codice lungo blocca la pagina.JavaScript - linguaggio, DOM ed eventi → (funzioni, DOM, eventi, event loop, delegazione degli eventi: lì Promise e fetch sono introdotte in breve, qui si approfondiscono e si usano in un client completo), HTML5 - struttura del documento, elementi semantici e formUn documento HTML5 ha la forma doctype (), html con lang, head (meta charset UTF-8, viewport, title, link, script) e body. Il parser HTML5 non dà mai errore: corregge (aggiunge il tbody, chiude i p). Gli elementi semantici (header, nav, main, section, article, aside, footer, figure, time) descrivono il ruolo del contenuto e danno ai lettori di schermo i punti di riferimento; div e span sono neutri. Link: a href (assoluto, relativo, #frammento, mailto), rel e target; immagini con alt obbligatorio. Tabelle: table, caption, thead, tbody, th con scope, td, colspan e rowspan, solo per dati. Form: action, method (GET mette i campi nella query string, per letture idempotenti e ricercabili; POST li mette nel corpo, per modificare lo stato), enctype (application/x-www-form-urlencoded predefinito, multipart/form-data per i file); si invia solo ciò che ha name; label for collega etichetta e campo; tipi di input (text, email, number, date, time, radio, checkbox, hidden...); validazione nativa con required, minlength, maxlength, min, max, step, pattern e type, aggirabile e quindi da ripetere sul server; novalidate la spegne. Accessibilità: lang, alt, label, gerarchia dei titoli, navigazione da tastiera, ARIA solo quando l'HTML nativo non basta.HTML5 - struttura del documento, elementi semantici e form → e Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →.
Richieste asincrone
JavaScript nel browser è a thread unico con un ciclo degli eventi (event loop): se una funzione aspettasse la rete, la pagina sarebbe bloccata. Perciò le operazioni lente sono asincrone: si avvia la richiesta, il programma prosegue, e il risultato arriva dopo, come evento, quando il codice corrente ha finito. Tre stili, dal più vecchio al più nuovo.
XMLHttpRequest
XMLHttpRequest (XHR) è l'oggetto storico, ancora presente in molto codice.
// xhr.js - lo stesso "elenco aule" con XMLHttpRequest: prima con le callback, poi avvolto in una Promise.
export function leggiAule(quandoPronto, inCasoDiErrore) {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/aule"); // metodo e URL; la richiesta è asincrona di default
xhr.setRequestHeader("Accept", "application/json");
xhr.onload = () => { // risposta completa arrivata, con QUALUNQUE codice di stato
if (xhr.status === 200) {
quandoPronto(JSON.parse(xhr.responseText));
} else {
inCasoDiErrore(`errore ${xhr.status}`);
}
};
xhr.onerror = () => inCasoDiErrore("rete non raggiungibile"); // errore di rete: nessuna risposta
xhr.send(); // non blocca la pagina: il risultato arriva a onload
}
// la stessa cosa come Promise: resolve = successo, reject = fallimento
export function leggiAuleConPromise() {
return new Promise((resolve, reject) => leggiAule(resolve, (messaggio) => reject(new Error(messaggio))));
}open(metodo, url)configura,send()parte (per unaPOSTcol corpo come argomento:xhr.send(JSON.stringify(dati))).- Il risultato non è il valore di ritorno di
send, ma arriva a una callback:onloadquando la risposta è completa (con qualunque codice di stato, quindi si controllaxhr.status),onerrorse la rete fallisce. xhr.responseTextè il corpo come testo; per il JSON si usaJSON.parse.- Con più richieste in sequenza le callback si annidano una dentro l'altra (callback hell), e la gestione degli errori si ripete in ogni livello. Le Promise nascono per questo: nel file,
leggiAuleConPromiseavvolge la callback connew Promise((resolve, reject) => ...).
Promise
Una Promise è un oggetto che rappresenta un valore che non c'è ancora. Ha tre stati: in attesa (pending), mantenuta (fulfilled, con un valore), rifiutata (rejected, con un errore); passa da pending a uno degli altri due una sola volta. Si usa con:
| Metodo | Quando parte | Restituisce |
|---|---|---|
.then(f) |
se mantenuta, con il valore | una nuova Promise (col risultato di f): per questo si concatena |
.catch(g) |
se rifiutata, con l'errore | una nuova Promise; se g restituisce un valore, la catena prosegue come riuscita |
.finally(h) |
sempre, alla fine | una nuova Promise con lo stesso esito (serve per ripulire) |
new Promise((resolve, reject) => reject(new Error("rifiutata")))
.then((v) => console.log("then", v)) // saltato: la Promise è rifiutata
.catch((e) => { console.log("catch:", e.message); return "recuperato"; })
.finally(() => console.log("finally"))
.then((v) => console.log("poi:", v));Esecuzione (provata con Node.js):
catch: rifiutata
finally
poi: recuperatoUn errore salta tutti i then fino al primo catch; il catch che restituisce un valore "guarisce" la catena.
async e await
async function restituisce sempre una Promise. Dentro, await promessa sospende la funzione finché la Promise non si risolve, e restituisce il valore (o lancia l'errore, catturabile con try/catch). La pagina non si blocca: mentre la funzione aspetta, il browser serve gli altri eventi. Si scrive come codice sequenziale, ma è la stessa cosa delle Promise.
async function f() {
console.log("b dentro f, prima di await");
await null; // cede il controllo: il resto riparte più tardi
console.log("d dentro f, dopo await");
}
console.log("a prima di f");
f();
console.log("c dopo la chiamata di f");a prima di f
b dentro f, prima di await
c dopo la chiamata di f
d dentro f, dopo awaitf() parte in modo sincrono fino al primo await; il resto entra in coda come microtask e va in esecuzione solo quando il codice corrente è finito. Regola generale dell'ordine: prima il codice sincrono, poi i microtask (reazioni delle Promise), poi i timer e gli altri eventi:
console.log("1 inizio");
setTimeout(() => console.log("4 timer"), 0);
Promise.resolve().then(() => console.log("3 promise"));
console.log("2 fine del codice sincrono");1 inizio
2 fine del codice sincrono
3 promise
4 timerPer più richieste indipendenti si lanciano insieme e si aspettano insieme: const [aule, prenotazioni] = await Promise.all([elencaAule(), elencaPrenotazioni(1, giorno)]). È più veloce di due await in fila, e rifiuta se una delle due fallisce.
fetch
fetch(url, opzioni) è l'API moderna (Fetch Standard, WHATWG): restituisce una Promisemethod, headers, body.
const risposta = await fetch("/api/prenotazioni", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ aula_id: 1, giorno: "2026-10-19", ora_inizio: "11:00", ora_fine: "12:30", studente: "Neri" }),
});L'oggetto Response ha status (numero), ok (vero per 200-299), statusText, headers e i metodi che leggono il corpo, anch'essi asincroni: await risposta.json() (analizza il JSON), .text(), .blob(). Il corpo si legge una volta sola.
Trappola principale.
fetchrifiuta la Promise solo se la rete fallisce (server irraggiungibile, offline: eccezioneTypeError). Una risposta404o500è una risposta riuscita dal punto di vista della rete: la Promise è mantenuta eokèfalse. Il codice deve sempre controllarerisposta.ok(ostatus).
Verificato su un server simulato (ok falso e status 404 per un'aula inesistente; eccezione senza rete):
fetch 404 -> ok: false status: 404 { errore: 'aula inesistente' }
fetch senza rete -> eccezione: TypeError Failed to fetchPer i filtri nella query si usa URLSearchParams, che codifica i caratteri speciali: new URLSearchParams({ giorno: "2026-10-19", aula: 1 }).toString() dà giorno=2026-10-19&aula=1. I cookie della sessione vengono inviati da fetch verso la stessa origine di default.
Il client dell'applicazione delle aule
Una pagina HTML con form e tabella vuota, uno script che fa tutto il lavoro. Si dividono i compiti in due file: api.js parla con il server; pagina.js parla con la pagina.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prenota un'aula</title>
<style>
.ok { color: #166534; }
.errore { color: #b91c1c; }
td, th { padding: .25rem .75rem; text-align: left; }
</style>
</head>
<body>
<h1>Prenotazione aule</h1>
<form id="modulo">
<label>Aula <select id="aula_id"></select></label>
<label>Giorno <input type="date" id="giorno" required></label>
<label>Dalle <input type="time" id="ora_inizio" required></label>
<label>Alle <input type="time" id="ora_fine" required></label>
<label>Studente <input type="text" id="studente" maxlength="40" required></label>
<button>Prenota</button>
</form>
<p id="messaggio" role="status"></p>
<table>
<thead><tr><th>Dalle</th><th>Alle</th><th>Studente</th><th></th></tr></thead>
<tbody id="righe"></tbody>
</table>
<script type="module" src="js/pagina.js"></script>
</body>
</html><script type="module" src="js/pagina.js">: uno script modulo viene eseguito dopo l'analisi dell'HTML (comedefer), in modalità rigorosa, con ambito proprio, e può usareimport/exporteawaital livello superiore. Va servito con HTTP (Tomcat), non aperto dafile://.- Ogni campo ha un
ide si trova condocument.getElementById; i bottoni di cancellazione sono creati da JavaScript.
// api.js - il "model" del client: parla con l'API REST, non conosce la pagina HTML.
const BASE = "/api"; // l'API sta sotto /api, alla radice del sito
export class ErroreApi extends Error {
constructor(stato, messaggio) {
super(messaggio);
this.stato = stato; // codice di stato HTTP della risposta
}
}
async function richiesta(metodo, percorso, corpo) {
const opzioni = { method: metodo, headers: { Accept: "application/json" } };
if (corpo !== undefined) {
opzioni.headers["Content-Type"] = "application/json";
opzioni.body = JSON.stringify(corpo);
}
const risposta = await fetch(`${BASE}${percorso}`, opzioni); // rifiuta solo se la rete fallisce
if (risposta.status === 204) {
return null; // successo senza corpo (DELETE)
}
const dati = await risposta.json().catch(() => null); // il corpo potrebbe non essere JSON
if (!risposta.ok) { // 4xx e 5xx NON fanno rifiutare fetch: si controllano a mano
throw new ErroreApi(risposta.status, dati?.errore ?? risposta.statusText);
}
return dati;
}
export const elencaAule = () => richiesta("GET", "/aule");
export function elencaPrenotazioni(aulaId, giorno) {
const parametri = new URLSearchParams({ giorno });
if (aulaId) {
parametri.set("aula", aulaId);
}
return richiesta("GET", `/prenotazioni?${parametri}`);
}
export const creaPrenotazione = (prenotazione) => richiesta("POST", "/prenotazioni", prenotazione);
export const cancellaPrenotazione = (id) => richiesta("DELETE", `/prenotazioni/${id}`);- Tutte le chiamate passano da
richiesta: costruisce le opzioni, aggiungeContent-Typee corpo solo se c'è un corpo, e trasforma gli errori HTTP in eccezioni. Chi la usa scrivetry/catchuna volta sola, senza ripetere il controllo diok. risposta.json().catch(() => null): se il corpo non è JSON non si rompe, l'errore resta quello di stato.dati?.errore ?? risposta.statusText: il messaggio del server ({"errore": ...}) se c'è, altrimenti la descrizione standard.204non ha corpo: si restituiscenullsenza chiamarejson().ErroreApiportastato, così la pagina può reagire diversamente a409.
// pagina.js - la "view" e il "controller" del client: legge il form, chiama api.js, aggiorna la tabella.
import { ErroreApi, elencaAule, elencaPrenotazioni, creaPrenotazione, cancellaPrenotazione } from "./api.js";
const $ = (id) => document.getElementById(id);
function messaggio(testo, errore = false) {
const p = $("messaggio");
p.textContent = testo; // textContent: il testo non viene mai interpretato come HTML
p.className = errore ? "errore" : "ok";
}
function riga(prenotazione) {
const tr = document.createElement("tr");
for (const testo of [prenotazione.ora_inizio, prenotazione.ora_fine, prenotazione.studente]) {
const td = document.createElement("td");
td.textContent = testo;
tr.append(td);
}
const bottone = document.createElement("button");
bottone.textContent = "Cancella";
bottone.dataset.id = prenotazione.id;
const tdAzione = document.createElement("td");
tdAzione.append(bottone);
tr.append(tdAzione);
return tr;
}
async function aggiorna() {
try {
const lista = await elencaPrenotazioni($("aula_id").value, $("giorno").value);
$("righe").replaceChildren(...lista.map(riga));
} catch (e) {
messaggio(`Impossibile leggere le prenotazioni: ${e.message}`, true);
}
}
$("modulo").addEventListener("submit", async (evento) => {
evento.preventDefault(); // niente invio classico del form: la pagina non si ricarica
const prenotazione = {
aula_id: Number($("aula_id").value),
giorno: $("giorno").value,
ora_inizio: $("ora_inizio").value,
ora_fine: $("ora_fine").value,
studente: $("studente").value,
};
const bottone = evento.submitter; // il bottone "Prenota" che ha inviato il form
bottone.disabled = true; // un secondo clic durante la richiesta non invia una seconda POST
try {
await creaPrenotazione(prenotazione);
messaggio("Prenotazione registrata.");
await aggiorna();
} catch (e) {
const testo = e instanceof ErroreApi && e.stato === 409 ? "Orario già occupato." : e.message;
messaggio(testo, true);
} finally {
bottone.disabled = false; // in ogni caso, riuscita o no, il bottone torna attivo
}
});
$("righe").addEventListener("click", async (evento) => { // un solo ascoltatore per tutti i bottoni
const id = evento.target.dataset.id;
if (!id) {
return;
}
try {
await cancellaPrenotazione(id);
messaggio("Prenotazione cancellata.");
await aggiorna();
} catch (e) {
messaggio(e.message, true);
}
});
$("aula_id").addEventListener("change", aggiorna);
$("giorno").addEventListener("change", aggiorna);
const aule = await elencaAule(); // await al livello superiore: consentito nei moduli
$("aula_id").replaceChildren(...aule.map((a) => new Option(`${a.nome} (${a.posti} posti)`, a.id)));
$("giorno").value = new Date().toISOString().slice(0, 10);
await aggiorna();Che cosa fa, in ordine:
- Avvio (le ultime righe):
await elencaAule()riempie il menu connew Option(testo, valore), imposta il giorno di oggi e carica le prenotazioni. Le funzioni dichiarate sopra sono già disponibili perché l'avvio è in fondo. aggiorna(): chiede l'elenco per aula e giorno e sostituisce le righe conreplaceChildren. È chiamata all'avvio, al cambio di aula o giorno e dopo ogni modifica.- Invio del form:
evento.preventDefault()ferma l'invio classico (che ricaricherebbe la pagina); si costruisce l'oggetto con i nomi dei campi dell'API (aula_id,ora_inizio...), si disabilita il bottonePrenota(evento.submitter) finché la richiesta non è finita, così un doppio clic non invia duePOST, e lo si riabilita nelfinally; si chiamacreaPrenotazione. Se riesce: messaggio eaggiorna(); se fallisce con409, "Orario già occupato."; altrimenti il messaggio del server (per esempio il400sulle ore). - Cancellazione: un solo gestore di
clicksultbody(delegazione degli eventi), perché le righe vengono create e distrutte di continuo;evento.target.dataset.idlegge l'attributodata-iddel bottone cliccato. - Nessun
innerHTMLcon dati del server:textContentassegna testo, mai HTML. Se lo studente si chiamasse<img src=x onerror=...>, coninnerHTMLil codice verrebbe eseguito (XSS); contextContentè solo una scritta.
Il codice è stato provato con Node.js: api.js contro un server simulato che rispetta il contratto (creazione 201, sovrapposizione 409, cancellazione 204, poi 404) e pagina.js con un DOM minimo simulato (elenco, invio valido, invio sovrapposto, click di cancellazione, click fuori dai bottoni).
MVC rivisto
Nel Web 1.0 tutti e tre i ruoli stavano sul server. Nell'applicazione Web 2.0 si spostano:
| Ruolo | Web 1.0 (server) | Web 2.0 (dopo la nota) |
|---|---|---|
| Model | JavaBean e DAO | JavaBean e DAO sul server; nel browser api.js come accesso ai dati remoti |
| View | JSP che produce HTML | il DOM costruito da pagina.js nel browser |
| Controller | servlet che sceglie la JSP | servlet REST (decide codici e dati) e gestori di eventi nel browser (decidono che cosa chiedere e come mostrare) |
| Risposta del server | pagina HTML | JSON |
| Stato dell'interfaccia | sessione sul server | nel browser (valori dei campi, righe mostrate) |
| Ricarica | tutta la pagina | solo la parte cambiata |
Conseguenze:
- Il server è senza stato e non sa nulla di HTML: la stessa API serve la pagina web, un'app mobile e altri programmi.
- La validazione si fa due volte: nel browser per la comodità (
required,type="time", messaggio immediato) e sempre sul server, perché il client può essere aggirato concurl. - Tutti i controlli di sicurezza (chi può cancellare che cosa) stanno sul server: il codice JavaScript è visibile e modificabile da chi lo scarica.
- Costi: il primo caricamento scarica anche JavaScript; i motori di ricerca vedono meno contenuto se questo è generato dallo script; due codici (Java e JavaScript) da mantenere; la gestione degli errori di rete è a carico del client. Le single page application portano il modello all'estremo (una sola pagina HTML, navigazione simulata da JavaScript).
Same-origin e CORS
Perché la pagina del progetto può chiamare /api/prenotazioni senza altre cure? Perché la pagina e l'API hanno la stessa origine.
Una origine (origin) è la terna schema, host, porta. Dalla pagina http://localhost:8080/prenota.html:
| URL richiesto da JavaScript | Stessa origine? | Perché |
|---|---|---|
http://localhost:8080/api/aule |
sì | stessa terna, il path non conta |
https://localhost:8080/api/aule |
no | schema diverso |
http://localhost:5500/api/aule |
no | porta diversa |
http://127.0.0.1:8080/api/aule |
no | host diverso (anche se è lo stesso computer) |
La same-origin policy (criterio della stessa origine) impedisce a uno script di leggere le risposte di un'altra origine: altrimenti una pagina qualsiasi, aperta mentre si è collegati alla banca, potrebbe leggere i dati dell'utente. La richiesta può partire; è la lettura della risposta che il browser blocca.
Il CORS (Cross-Origin Resource Sharing, parte del Fetch Standard) è il meccanismo con cui il server autorizza altre origini, con header di risposta:
| Header | Significato |
|---|---|
Access-Control-Allow-Origin |
l'origine autorizzata (o * per tutte, solo per dati pubblici) |
Access-Control-Allow-Methods |
metodi permessi |
Access-Control-Allow-Headers |
header permessi nella richiesta (Content-Type) |
Access-Control-Max-Age |
per quanto tempo il browser può ricordare il permesso |
Le richieste "semplici" (GET, HEAD, POST con Content-Type del tipo form o text/plain) partono subito, e il browser guarda la risposta. Le altre, tra cui una POST con application/json e una DELETE, sono precedute da una richiesta preliminare (preflight) OPTIONS con l'origine e il metodo desiderato; solo se la risposta le autorizza il browser invia la richiesta vera.
OPTIONS /api/prenotazioni HTTP/1.1
Origin: http://localhost:5500
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-typeHTTP/1.1 204
Access-Control-Allow-Origin: http://localhost:5500
Vary: Origin
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 600In Tomcat si fa con un filtro sull'API (provato con richieste simulate: origine ammessa e OPTIONS 204 con gli header e la servlet non viene chiamata; origine non ammessa o assente nessun header, la richiesta prosegue):
package it.unipd.aule.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
/** Permette a pagine servite da ALTRE origini (elencate) di chiamare l'API: CORS. Serve solo se la pagina non è nello stesso war. */
@WebFilter("/api/*")
public class CorsFilter extends HttpFilter {
private static final Set<String> AMMESSE = Set.of("http://localhost:5500", "https://aule.example.org");
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain catena)
throws IOException, ServletException {
String origine = req.getHeader("Origin"); // lo aggiunge il browser alle richieste cross-origin
if (origine != null && AMMESSE.contains(origine)) {
resp.setHeader("Access-Control-Allow-Origin", origine); // mai "*" se l'API usa cookie o credenziali
resp.addHeader("Vary", "Origin"); // la risposta dipende dall'origine: le cache lo devono sapere
if ("OPTIONS".equals(req.getMethod())) { // richiesta preliminare (preflight)
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
resp.setHeader("Access-Control-Max-Age", "600"); // il browser può ricordare il permesso per 10 minuti
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
return; // il preflight non arriva alla servlet
}
}
catena.doFilter(req, resp);
}
}Il CORS è una protezione del browser: curl o un altro server ignorano gli header. Non sostituisce l'autenticazione, e Access-Control-Allow-Origin: * non si usa con cookie o credenziali (con credentials: "include" nel fetch servono un'origine esplicita e Access-Control-Allow-Credentials: true). Per l'applicazione delle aule, servita dal war insieme all'API, il filtro non serve: è lo scenario di una pagina sviluppata con un altro server (localhost:5500).
Errori tipici
- Credere che
fetchrifiuti su404o500: senzaif (!risposta.ok)gli errori passano inosservati. - Dimenticare
await(si lavora con una Promise invece che col valore), o chiamarerisposta.json()senzaawait; leggere il corpo due volte. - Dimenticare
Content-Type: application/jsonnellaPOST: il server risponde415. - Dimenticare
preventDefault()nel gestore disubmit: la pagina si ricarica. - Costruire righe con
innerHTMLe dati del server: XSS. UsaretextContent. - Un gestore di click per ogni bottone creato dinamicamente, invece della delegazione.
- Fidarsi della sola validazione del browser; mettere controlli di accesso solo in JavaScript.
- Ricevere un errore di CORS e aggiungere
*ovunque, o credere che il problema sia nel codice JavaScript: si risolve nel server. - Aprire la pagina da
file://con moduli efetch: originenull, richieste bloccate.
Domande d'esame
1. Che cos'è AJAX e che cosa cambia rispetto al Web 1.0? Come rivede il paradigma MVC?
Traccia. JavaScript fa richieste HTTP in background e aggiorna solo il DOM, senza ricaricare. Il server offre dati (REST/JSON) e resta senza stato; la view passa nel browser (DOM), il controller della pagina sono i gestori di eventi, il model remoto è accessibile con fetch; la validazione e i controlli di sicurezza restano comunque anche sul server.
2. Scrivere con fetch e async/await la funzione che cancella la prenotazione id e segnala l'errore se la risposta non è 204. Che cosa succede con un 404?
Traccia. const r = await fetch("/api/prenotazioni/" + id, { method: "DELETE" }); if (r.status !== 204) throw new Error(...). Con un 404 la Promise di fetch è mantenuta (c'è una risposta), ok è falso: serve il controllo esplicito. La Promise sarebbe rifiutata solo senza rete.
3. Una pagina su http://localhost:5500 chiama l'API su http://localhost:8080 e il browser blocca la risposta. Perché, e come si risolve? Perché prima della POST JSON c'è una OPTIONS?
Traccia. Origini diverse (porta): same-origin policy. Il server deve inviare Access-Control-Allow-Origin con l'origine della pagina (CORS), in un filtro. Una POST con application/json non è una richiesta semplice: il browser chiede prima con OPTIONS (preflight) se metodo e header sono ammessi.
Versione ripasso
AJAX. JavaScript fa richieste HTTP in background e aggiorna solo una parte del DOM, senza ricaricare la pagina; il server offre dati (JSON, Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →). JavaScript è a thread unico con event loop: le operazioni lente sono asincrone e il risultato arriva dopo, quando il codice corrente è finito.
XMLHttpRequest. open(metodo, url), setRequestHeader, send(); il risultato arriva a callback: onload (qualunque stato: controllare xhr.status; corpo in responseText, poi JSON.parse) e onerror (rete).
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/aule");
xhr.onload = () => { if (xhr.status === 200) mostra(JSON.parse(xhr.responseText)); };
xhr.onerror = () => console.log("rete non raggiungibile");
xhr.send();Callback annidate = callback hell: le Promise avvolgono la callback con new Promise((resolve, reject) => ...).
Promise. Stati: in attesa, mantenuta, rifiutata (una sola transizione). .then (se mantenuta), .catch (se rifiutata), .finally (sempre): ciascuno restituisce una nuova Promise, per questo si concatenano. Un errore salta i then fino al catch; un catch che restituisce un valore fa proseguire la catena.
new Promise(rifiuta).then(...).catch(...).finally(...).then(...)
catch: rifiutata / finally / poi: recuperatoasync/await. async function restituisce una Promise; await sospende la funzione finché la Promise si risolve e restituisce il valore (o lancia l'errore, try/catch). La pagina non si blocca. Ordine di esecuzione: codice sincrono, poi microtask (promise), poi timer.
a prima di f / b dentro f, prima di await / c dopo la chiamata di f / d dentro f, dopo await
1 inizio / 2 fine del codice sincrono / 3 promise / 4 timerPromise.all([a, b]) lancia le richieste insieme (rifiuta se una fallisce): const [aule, prenotazioni] = await Promise.all([elencaAule(), elencaPrenotazioni(1, giorno)]).
fetch. fetch(url, { method, headers, body }) restituisce Promise<Response>; Response: ok (200-299), status, statusText, headers, json(), text() (asincroni, corpo letto una volta).
const risposta = await fetch("/api/prenotazioni", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ aula_id: 1, giorno: "2026-10-19", ora_inizio: "11:00", ora_fine: "12:30", studente: "Neri" }),
});
if (!risposta.ok) throw new Error(`errore ${risposta.status}`);- Trappola:
fetchrifiuta solo per errori di rete (TypeError);404e500sono risposte conokfalso: controllare sempreok. Esempio:fetch 404 -> ok: false status: 404. URLSearchParams({ giorno: "2026-10-19", aula: 1 })giorno=2026-10-19&aula=1. I cookie vanno alla stessa origine di default.
Client delle aule. prenota.html (form con id, tbody id="righe", <script type="module" src="js/pagina.js">: eseguito dopo l'HTML, ambito proprio, import, await al livello superiore; servito via HTTP). api.js = accesso ai dati remoti, pagina.js = pagina.
async function richiesta(metodo, percorso, corpo) {
const opzioni = { method: metodo, headers: { Accept: "application/json" } };
if (corpo !== undefined) {
opzioni.headers["Content-Type"] = "application/json";
opzioni.body = JSON.stringify(corpo);
}
const risposta = await fetch(`${BASE}${percorso}`, opzioni);
if (risposta.status === 204) return null; // DELETE: nessun corpo
const dati = await risposta.json().catch(() => null);
if (!risposta.ok) throw new ErroreApi(risposta.status, dati?.errore ?? risposta.statusText);
return dati;
}
export const creaPrenotazione = (p) => richiesta("POST", "/prenotazioni", p);
export const cancellaPrenotazione = (id) => richiesta("DELETE", `/prenotazioni/${id}`);ErroreApihastato: la pagina distingue409("Orario già occupato."). I filtri vanno in query conURLSearchParams.pagina.js: avvio (elencaAule()riempie il menu connew Option, giorno di oggi,aggiorna());aggiorna()sostituisce le righe conreplaceChildren;submitconevento.preventDefault(), oggetto con i nomi dell'API,creaPrenotazione, poiaggiorna(); delegazione: un soloclicksultbody,evento.target.dataset.idleggedata-id.- Testo del server con
textContent, maiinnerHTML(XSS).
MVC rivisto.
| Ruolo | Web 1.0 | Web 2.0 |
|---|---|---|
| Model | JavaBean e DAO | JavaBean e DAO sul server, api.js nel browser |
| View | JSP | DOM costruito da pagina.js |
| Controller | servlet che sceglie la JSP | servlet REST (dati e codici) e gestori di eventi |
| Risposta | HTML | JSON |
| Stato | sessione sul server | nel browser; server senza stato |
Validazione nel browser (comodità) e sul server (obbligatoria); sicurezza solo sul server; costi: primo caricamento, SEO, due linguaggi, errori di rete a carico del client. Single page application = una sola pagina HTML.
Same-origin e CORS. Origine = schema, host, porta (http://localhost:8080; diverse: https://..., porta 5500, 127.0.0.1). La same-origin policy impedisce di leggere risposte di altre origini (la richiesta può partire). CORS: il server autorizza con Access-Control-Allow-Origin (origine esplicita, * solo per dati pubblici e mai con credenziali), Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age. Richieste semplici partono subito; POST con application/json e DELETE hanno prima un preflight OPTIONS (Origin, Access-Control-Request-Method).
@WebFilter("/api/*")
public class CorsFilter extends HttpFilter {
protected void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain catena) throws IOException, ServletException {
String origine = req.getHeader("Origin");
if (origine != null && AMMESSE.contains(origine)) {
resp.setHeader("Access-Control-Allow-Origin", origine);
resp.addHeader("Vary", "Origin");
if ("OPTIONS".equals(req.getMethod())) { // preflight
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
resp.setHeader("Access-Control-Max-Age", "600");
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
}
catena.doFilter(req, resp);
}
}CORS è una protezione del browser: curl la ignora, non sostituisce l'autenticazione. Nell'applicazione delle aule (pagina e API nello stesso war) non serve.
Errori tipici
fetchsenza controllo diok;awaitdimenticato; corpo letto due volte; nienteContent-Type: application/json(risposta415).- Niente
preventDefault()nelsubmit;innerHTMLcon dati del server; un gestore per ogni bottone dinamico. - Validazione e controlli di accesso solo in JavaScript; errore CORS "risolto" con
*; pagina aperta dafile://.
Domande d'esame (tracce)
- AJAX = richieste in background e DOM parziale; MVC rivisto: view nel browser, server senza stato che dà dati.
fetch(..., { method: "DELETE" }), controllor.status !== 204; con404la Promise è mantenuta eokè falso.- Origini diverse (porta): CORS con
Access-Control-Allow-Origin; preflightOPTIONSperché laPOSTJSON non è semplice.
Collegamenti: JavaScript - linguaggio, DOM ed eventiJavaScript (standard ECMAScript, Ecma-262) è il linguaggio dei browser: tipizzato dinamicamente, con sette tipi primitivi (number, string, boolean, undefined, null, bigint, symbol) e gli oggetti (anche array e funzioni); typeof null vale "object" per un errore storico; == converte i tipi, === no (si usa sempre ===); valori falsy: false, 0, 0n, "", null, undefined, NaN. let e const hanno visibilità di blocco e una zona morta prima della dichiarazione, var visibilità di funzione e hoisting; const impedisce di riassegnare il nome, non di modificare l'oggetto. Funzioni: dichiarazione (hoisted), espressione, arrow function (this lessicale), parametri predefiniti, rest e spread, destructuring. Array: map, filter, reduce, find, some, every, sort (senza comparatore ordina come stringhe). Closure: una funzione ricorda le variabili dell'ambiente in cui è nata. Eventi: addEventListener; propagazione in tre fasi (cattura, target, bolla), stopPropagation, preventDefault, delegazione con event.target e closest. DOM: albero di nodi; querySelector, createElement, append, replaceChildren, textContent (sicuro) contro innerHTML (rischio di XSS), classList, dataset. Moduli ES (type="module", import e export) sono differiti e in modalità strict. Event loop: un solo thread, call stack, coda dei task (setTimeout, eventi) e coda dei microtask (promise, await) svuotata dopo ogni task: l'esecuzione asincrona (fetch, async e await, Promise.all) è alla base di AJAX e il codice lungo blocca la pagina.JavaScript - linguaggio, DOM ed eventi →, JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →, Esercizio - Pagina AJAX per creare e cancellare prenotazioni (applicazione web).