Salta al contenuto
Note per Studenti AJAX e fetch - applicazioni Web 2.0 e MVC rivisto

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.

javascript
// 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 una POST col corpo come argomento: xhr.send(JSON.stringify(dati))).
  • Il risultato non è il valore di ritorno di send, ma arriva a una callback: onload quando la risposta è completa (con qualunque codice di stato, quindi si controlla xhr.status), onerror se la rete fallisce.
  • xhr.responseText è il corpo come testo; per il JSON si usa JSON.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, leggiAuleConPromise avvolge la callback con new 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)
javascript
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: recuperato

Un 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.

javascript
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 await

f() 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:

javascript
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 timer

Per 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 Promise. Le opzioni sono un oggetto con method, headers, body.

javascript
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. fetch rifiuta la Promise solo se la rete fallisce (server irraggiungibile, offline: eccezione TypeError). Una risposta 404 o 500 è una risposta riuscita dal punto di vista della rete: la Promise è mantenuta e ok è false. Il codice deve sempre controllare risposta.ok (o status).

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 fetch

Per 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.

html
<!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 (come defer), in modalità rigorosa, con ambito proprio, e può usare import/export e await al livello superiore. Va servito con HTTP (Tomcat), non aperto da file://.
  • Ogni campo ha un id e si trova con document.getElementById; i bottoni di cancellazione sono creati da JavaScript.
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, aggiunge Content-Type e corpo solo se c'è un corpo, e trasforma gli errori HTTP in eccezioni. Chi la usa scrive try/catch una volta sola, senza ripetere il controllo di ok.
  • 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.
  • 204 non ha corpo: si restituisce null senza chiamare json().
  • ErroreApi porta stato, così la pagina può reagire diversamente a 409.
javascript
// 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:

  1. Avvio (le ultime righe): await elencaAule() riempie il menu con new Option(testo, valore), imposta il giorno di oggi e carica le prenotazioni. Le funzioni dichiarate sopra sono già disponibili perché l'avvio è in fondo.
  2. aggiorna(): chiede l'elenco per aula e giorno e sostituisce le righe con replaceChildren. È chiamata all'avvio, al cambio di aula o giorno e dopo ogni modifica.
  3. 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 bottone Prenota (evento.submitter) finché la richiesta non è finita, così un doppio clic non invia due POST, e lo si riabilita nel finally; si chiama creaPrenotazione. Se riesce: messaggio e aggiorna(); se fallisce con 409, "Orario già occupato."; altrimenti il messaggio del server (per esempio il 400 sulle ore).
  4. Cancellazione: un solo gestore di click sul tbody (delegazione degli eventi), perché le righe vengono create e distrutte di continuo; evento.target.dataset.id legge l'attributo data-id del bottone cliccato.
  5. Nessun innerHTML con dati del server: textContent assegna testo, mai HTML. Se lo studente si chiamasse <img src=x onerror=...>, con innerHTML il codice verrebbe eseguito (XSS); con textContent è 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 con curl.
  • 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.

http
OPTIONS /api/prenotazioni HTTP/1.1
Origin: http://localhost:5500
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
http
HTTP/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: 600

In Tomcat si fa con un filtro sull'API (provato con richieste simulate: origine ammessa e OPTIONS →\to 204 con gli header e la servlet non viene chiamata; origine non ammessa o assente →\to nessun header, la richiesta prosegue):

java
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 fetch rifiuti su 404 o 500: senza if (!risposta.ok) gli errori passano inosservati.
  • Dimenticare await (si lavora con una Promise invece che col valore), o chiamare risposta.json() senza await; leggere il corpo due volte.
  • Dimenticare Content-Type: application/json nella POST: il server risponde 415.
  • Dimenticare preventDefault() nel gestore di submit: la pagina si ricarica.
  • Costruire righe con innerHTML e dati del server: XSS. Usare textContent.
  • 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 e fetch: origine null, 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).

javascript
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: recuperato

async/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 timer

Promise.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).

javascript
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: fetch rifiuta solo per errori di rete (TypeError); 404 e 500 sono risposte con ok falso: controllare sempre ok. Esempio: fetch 404 -> ok: false status: 404.
  • URLSearchParams({ giorno: "2026-10-19", aula: 1 }) →\to 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.

javascript
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}`);
  • ErroreApi ha stato: la pagina distingue 409 ("Orario già occupato."). I filtri vanno in query con URLSearchParams.
  • pagina.js: avvio (elencaAule() riempie il menu con new Option, giorno di oggi, aggiorna()); aggiorna() sostituisce le righe con replaceChildren; submit con evento.preventDefault(), oggetto con i nomi dell'API, creaPrenotazione, poi aggiorna(); delegazione: un solo click sul tbody, evento.target.dataset.id legge data-id.
  • Testo del server con textContent, mai innerHTML (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).

java
@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

  • fetch senza controllo di ok; await dimenticato; corpo letto due volte; niente Content-Type: application/json (risposta 415).
  • Niente preventDefault() nel submit; innerHTML con dati del server; un gestore per ogni bottone dinamico.
  • Validazione e controlli di accesso solo in JavaScript; errore CORS "risolto" con *; pagina aperta da file://.

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" }), controllo r.status !== 204; con 404 la Promise è mantenuta e ok è falso.
  • Origini diverse (porta): CORS con Access-Control-Allow-Origin; preflight OPTIONS perché la POST JSON 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).

Esercizi su questo argomento

Teoria collegata