Untitled Document
   
 
Redirect dopo Post
di Michael Jouravlev, traduzione di Francesco Saliola

Tutti i programmi interattivi forniscono due funzioni basilari: ricevere l'input dell'utente e mostrare i conseguenti risultati. Le web application implementano questo comportamento usando due metodi HTTP che sono, rispettivamente, POST e GET. Questo semplice protocollo va incontro a dei problemi, però, quando l'applicazione restituisce una pagina web in risposta a una richiesta POST. Le peculiari caratteristiche del metodo POST, in combinazione con le idiosincrasie dei diversi browser conducono spesso a spiacevoli inconvenienti sperimentati dagli utenti e possono produrre uno stato non corretto nell'applicazione server. Questo articolo mostra come progettare una web application che si comporta nel modo giusto attraverso l'uso della redirezione.

 

Il problema del doppio invio
I due metodi HTTP di request usati più frequentemente sono GET e POST. GET recupera risorse da un server web, le quali sono identificate dalla collocazione base ed eventualmente da parametri di ricerca opzionali.. Generalmente, i parametri della request GET vengono utilizzati per restringere l'ambito dei risultati e non cambiano lo stato del server. La medesima request GET può essere inviata al server per tutte le volte che sia ritenuto necessario.
Al contrario, i parametri presenti in una request di tipo POST contengono in genere dei dati input che possono modificare lo stato dell'applicazione sul server. Inviare i medesimi dati per due volte consecutive può produrre dei risultati non desiderati, come un doppio prelievo da un conto corrente bancario o l'aggiunta di uno stesso articolo per due volte nel carrello di un sito di commercio elettronico. L'invio dei medesimi dati per più di una sola volta in una request POST è una situazione non desiderabile e prende il nome di "problema del doppio invio".
Prendiamo il caso d'uso standard con un FORM HTML inviato al server. I dati contenuti nel form vengono elaborati e immagazzinati nel database e, successivamente, il server invia una pagina di risposta contenente i risultati dell'operazione.


Figura 1

Nel caso appena visto, la medesima request POST può essere inviata nuovamente secondo una delle seguenti tre possibilità:

  • ricaricare la pagina di risultati utilizzando la funzionalità Refresh/Reload del browser (ricaricamento esplicito della pagina, reinvio implicito della request);
  • effettuare un click sui bottoni Back/Indietro e poi su Forward/Avanti del browser (ricaricamento implicito della pagina, reinvio implicito della request);
  • ritornare nuovamente al FORM HTML dopo averlo inviato e premere nuovamente il bottone di Invio sul form (reinvio esplicito della request).

Proprio in considerazione dell'importanza dei dati POST, i browser mostrano un avviso quando una medesima request POST sta per essere nuovamente inviata al server. Ma si tratta di un messaggio troppo tecnico e poco comprensibile per l'utente medio e, inoltre, alcuni browser non chiedono affatto tale conferma. A causa di ciò, molti siti web mostrano dei messaggi di avvertimento: quante volte vi capita di vedere messaggi del tipo
"Per favore, non premere il bottone Back/indietro e non ricaricare questa pagina!" quanfo effettuate un pagamento online?


I messaggi di avvertimento e le finestre di dialogo che chiedono conferma tendono però a rendere l'interfaccia più disordinata e a innervosire l'utente, non mettendolo a suo agio e facendolo preoccupare per la paura di commettere costosi errori. Se un sito web fa affidamento sugli avvertimenti prodotti dal browser ma non controlla realmente il problema del doppio invio, il database del server può essere sottoposto a modifiche sbagliate, mentre l'utente perderà fiducia nelle transazioni via Internet.
È possibile liberarsi di questi fastidiosi avvertimenti? Sì. Il metodo di invio di FORM HTML può essere cambiato da POST a GET. Ai browser non viene rischiesto di proporre una finestra di conferma quando viene reinviata una request GET, pertanto questo cambiamento rende l'interfaccia utente più amichevole. Ma questa "soluzione" infrange la semantica del metodo GET: non impedisce un reinvio, ma si limita a nascondere il problema all'utente.

 

Il pattern PRG
La risposta al problema del doppio invio è la redirezione. Si tratta di una tecnica nota, ma che non è ancora diventata uno standard per i risultati "successivi al POST". Per quanto ne so, non ha un nome ben definito: proporrei allora di chiamare questa tecnica "pattern PRG". dove l'acronimo sta per POST - REDIRECT - GET.
Il pattern PRG suddivide una richiesta in due parti: invece di restituire una pagina di risultato immediatamente in risposta a una request POST, il server risponde con una redirezione alla pagina risultante. Il browser, quindi, caricherà la pagina di risultato come se si trattasse di una risorsa separata. Dopo tutto, sono due i compiti diversi che vanno svolti: primo un POST dei dati di input al server, secondo, un GET dei dati di output al cliente.

 


Figura 2


Questo approccio fornisce una soluzione pulita basata sull'architettura Model-View-Controller. Tutti i dati di input vengono salvati, in modo permanente o temporaneo, nel Model sul server durante la prima fase. La seconda fase carica una View che riflette lo stato corrente del Model. Quando un utente prova a effettuare un aggiornamento della pagina risultante, il browser provvede a reinviare una request GET "vuota" al server: vuota perché questa request non contiene alcun dato di input e quindi non modifica lo stato del server. Si limita solamente a ricaricare la vista precedente. Se lo stato del server non è stato modificato da altri processi/utenti, il server risponde con la medesima pagina inviata prima dell'operazione di refresh.
Caricare le risorse utilizzando il metodo GET è la pietra miliare dell'approccio suggerito in questo articolo. La pagina caricata con una request GET può essere aggiornata in maniera sicura e trasparente. Sicura poiché non vengono inviati dati di input al server. Trasparente perché il browser non mostra messaggi di avvertimento. Il veicolo che rende possibile la transizione da POST a GET è la redirezione.
Con questa tecnica, il feel dell'utente migliora in maniera enorme. Niente più messaggi inquietanti con avvertimenti di difficile interpretazione; niente più brividi nel fare click sui pulsanti di Back/Indietro, Forward/Avanti o Refresh/Aggiorna; nessuna paura di fare danni sui dati del server. Il pulsante di Refresh/Aggiorna ricarica la pagina con i risultati con una semplice request GET. Il pulsante Back/Indietro riporta un utente alla pagina con il form. Un click susseguente sul pulsante Forward/Avanti ricarica la pagina di risultato usando di nuovo una GET. Assoluta libertà di navigazione?
Un momento… Che cosa accade se si fa click sul pulsante Submit/Invia del form dopo essere ritornati alla pagina in cui è contenuto? Il form verrebbe inviato nuovamente, no? E quindi a che servirebbero tutti gli sforzi per la redirezione, se si limitano solo i problemi derivanti da un'inavvertito aggiornamento della pagina?

 

Mantenere viva la vista
I browser non hanno sempre mantenuto in cache le pagine web. Agli esordi di Internet, le cose erano molto semplici: dato a un browser il medesimo indirizzo , il browser recuperava sempre la stessa risorsa dal server, per tutte le volte. I browser moderni sono più intelligenti: sulla base di diversi fattori, tentano di determinare se la pagina deve essere caricata ex novo dal server oppure no. In caso negativo, recuperano la pagina da una cache in cui era stata precedentemente salvata, con un netto risparmio di traffico e tempo, molto importante specie per le connessioni non a banda larga.
Ma la comodità del salvataggio in caching influenza il comportamento standard. Ecco la domanda: che cosa vedrebbe un utente se, dopo aver inviato un form, fa click sul pulsante Back/Indietro del browser? Lo stesso form appena inviato con i valori immessi? Perché? Perché il browser ha salvato la pagina nella cache nel caso potesse essere ancora necessaria?
Be', dimenticatevi browser intelligenti e cache delle pagine: ciascuna finestra o pagina in una web application interattiva è una View che rappresenta un Model applicativo. Affinché la View sia corretta e coerente con il Model deve essere rigenerata nuovamente ogni volta che viene presentata all'utente.
In parole pover: il caching deve essere proibito nelle web application. Libri, vocabolari, immagini online possono essere salvati in cache. Ma per favore, caro browser, non salvare istantanee di un programma "vivo", poiché esse potrebbero non rappresentare più lo stato corrente del Model. Va già male quando una vista ormai superata viene visionata (meglio se salvi le foto delle modelle nude…), ma è dieci volte peggio quando una vista ormai scaduta viene utilizzata per modificare il modello.
Vi faccio nuovamente la stessa domanda: che cosa vedrebbe un utente se fa click sul pulsante Back/Indietro del browser, dopo aver inviato un form? Già sapete la risposta corretta: l'utente di una web application ben progettata vedrebbe una vista che rappresenta lo stato corrente del modello. Questa View sarebbe presentata in una maniera tale da rendere impossibile il reinvio dei dati già inviati precedentemente.

 

Un nuovo trucco per il vecchio FORM
Guardiamo da vicino al caso d'uso standard con un FORM HTML e una pagina di risultati. Il form può essere utilizzato per modificare un'entità presente nell'applicazione o per crearne una nuova. Dopo che il modulo sarà stato inviato, i suoi dati saranno immagazzinati nel database e i risultati dell'operazione saranno mostrati come vista.
Secondo il pattern PRG, la pagina di risultato non deve essere restituita come response a una request POST, poiché un tentativo di ricaricarla causerebbe i problemi connessi con il doppio invio. Al contrario, il browser deve caricare la pagina di risultato separatamente, usando il metodo GET.
Possiamo definire i seguenti moduli di elaborazione (action, se vogliamo parlare in gergo Struts) per il presente caso d'uso: Create Item (crea l'elemento), Display Item (mostra l'elemento), Store Item (salva l'elemento) e Display Stored (mostra l'elemento già salvato).
Questi moduli vengono combinati in coppie input/output:

  • Create Item/Display Item - crea un nuovo elemento vuoto, poi mostra il nuovo elemento usando il form HTML Item Page HTML che permette di immettere i valori dell'elemento;
  • Store Item/Display Stored - salva l'elemento, poi mostra l'elemento salvato (reso persistente nel database) in modalità solo lettura su una pagina Stored Result;
  • Store Item/Display Item - se il salvataggio in persistenza non va a buon fine, mostra un elemento non valido insieme agli errori, usando lo stesso form HTML;
  • Display Item viene mostrata separatamente per visualizzare e aggiornare un elemento già esistente nel database.


Figura 3 - Moduli di elaborazione: creazione e modifica.


1 - Create Item viene chiamato da un link, su qualche altra pagina web, quando deve essere creato un nuovo oggetto. Questa azione costruisce un oggetto vuoto dell'applicazione e lo salva in un'area temporanea denominata Current Items, la quale può essere salvata nel database o nella sessione; successivamente avviene una redirezione a Display Item.
2.1 - Display Item carica da Current Items l'oggetto business costruito e lo mostra sulla Item Page, che è il form HTML. Questo modulo può essere aggiornato in ogni momento, tanto il browser si limiterà a richiedere l'azione Display Item per ottenere e mostrare di nuovo il business object.
2.2 - L'utente compila il form HTML con i valori dell'oggetto e lo invia all'azione Store Item. Se l'oggetto non è accettato, viene mantenuto nell'area Current Items, il server effettua una redirezione all'azione Display Item, la quale legge gli oggetti non validi insieme con i messaggi di errore da Current Items e mostra nuovamente il form. Se la Item Page deve essere aggiornata, carica nuovamente lo stesso oggetto dalla Current Items.
3 - Se l'oggetto è accettato dall'azione Store Item, viene resto persistente nel database e rimosso dall'area temporanea. Dopo questo, il browser viene rediretto a una azione Display Stored che mostra la pagina Stored Result. Può mostrare l'oggetto che è stato appena resa persistente. La pagina di risultato può essere tranquillamente sottoposta a refresh: caricherà nuovamente l'oggetto presente nel database.

Se un utente fa click sul bottone Back/Indietro della pagina risultante (3) dopo che un nuovo oggetto sia stato creato e salvato con successo, ritornerà all'azione Display Item (2). L'oggetto temporaneo è già stato rimosso dall'area temporanea. Display Item non ha nulla da mostrare e mostrerà una pagina di errore invece del form per immettere i dati dell'elemento, notificando all'utente che l'oggetto non può essere mostrato semplicemente perché non esiste più. E in questo modo l'utente non potrà reinviare nuovamente l'oggetto.
Una situazione simile dovrebbe verificarsi se, durante la creazione di un nuovo oggetto, l'utente abbandona la pagina con il form (2) per ritornare a una pagina che la precede (1). Per l'applicazione ciò significa che l'utente ha deciso di eliminare il nuovo oggetto. Il nuovo oggetto viene rimosso da Current Items. Quando l'utente fa click sul pulsante Forward e ritorna a Display Item (2), vedrà un errore di tipo "Object not found".
È possibile agire in maniera migliore, invece di mostrare una pagina di errore quando un oggetto è ormai stato rimosso dall'area temporanea. Create Item genera degli ID di oggetto ed effettua una redireazione a Display Item con l'ID dell'oggetto come parametro di request. L'azione Display Item legge l'oggetto dalla sessione e confronta il suo ID con quello passato come argomento, poi mostra l'oggetto all'utente. Dopo che l'utente ha immesso i valori dell'oggetto e li ha inviati a Store Item, l'ID di oggetto diventa la chiave primaria per tale oggetto.
Ora, quando l'utente ritorna indietro dalla pagina con i risultati (3) dopo aver inviato un oggetto, il browser invoca l'azione Display Item che passa ad esso la stessa request, contenente l'ID di oggetto (2). L'oggetto è stato rimosso dalla sessione, ma è stato salvato nel database. L'azione Display Item legge l'oggetto dal database, lo copia in Current Items e lo mostra all'utente. A seconda delle regole dell'applicazione, questo oggetto può essere reso di sola lettura, in maniera tale che il form diventa una pagina semplice, che mostra il contenuto dell'oggetto ma non permette un suo nuovo invio. Oppure, al contrario, il form potrà consentire modifiche e reinvio delle stesse. Nell'ultimo caso, il titolo del form dovrebbe cambiare da "Crea nuovo oggetto" a "Modifica oggetto esistente". Se l'utente invia questo oggetto, non viene considerato un caso di doppio invio. Si tratta di una modifica intenzionale di un oggetto esistente.
La modifica di oggetti esistenti è facile: si tratta di un caso già trattato, almeno nei suoi aspetti basilari. Ci occorre solamente di assicurarsi che Display Item non faccia differenza tra l'oggetto nuovo e quello esistente. Display Item accetta l'ID dell'oggetto come parametro di request, poi ricerca il business object innanzitutto nei Current Items. Se l'oggetto non viene trovato, esso viene ricercato nel database, copiato in un'area temporanea e poi mostrato (2). Dopo l'aggiornamento dell'oggetto e il suo invio, esso viene salvato nel database. Quando l'untente fa click sul pulsante Back/Indietro a partire dalla pagina di risultato (3), la form dell'elemento ricarica nuovamente l'oggetto dal database (2) in maniera che possa essere modificato e reinviato ancora una volta. Si tratta di un caso di doppio invio? No. Si tratta di una modifica deliberata di uno stesso oggetto da parte di un utente. Ovviamente, è possibile creare tutte le business rule che si desiderano, anche quella che impedisce di modificare un oggetto prima che sia trascorso un certo tempo dalla sua creazione o ultima modifica.
Completiamo il caso d'uso analizzando le modalità con cui l'oggetto viene eliminato.



Figura 4 - Moduli di elaborazione: eliminazione.


L'eliminazione è semplice. Si prende l'ID come parametro di request, lo si passa alla azione Delete Item (4) che cancella l'elemento nel Model ed effettua la redirezione alla pagina di risultato (5) la quale verifica con il database che quel particolare oggetto non esiste più. La pagina di risultato può tranquillamente essere ricaricata senza produrre un'altra richiesta di cancellazione e senza messaggi di avvertimento. Quando viene utilizzato il pulsante Back/Indietro sulla pagina di risultati (5), il browser ritorna alla pagina che invoca la azione Delete Item (4). Se questa azione è nuovamente chiamata con lo stesso ID di oggetto, essa verrà applicata al Model, ottenendo un'eccezione di "oggetto non trovato" e la corrispondente pagina di errore. Anche qui, non si tratta di un caso di doppio invio, ma di un tentativo esplicito di cancellare nuovamente il medesismo oggetto… che tanto è già stato cancellato.
Penso che l'idea generale sia chiara. Ma prendiamo un altro esempio veloce: il negozio online.
Salvare alcuni articoli identici nel cestino non è un problema fino a quando l'utente sta ancora facendo le sue scelte. Basta mostrare il contenuto esatto del cestino e la quantità di ciascun articolo. Quel che davvero conta è di essere sicuri che il pagamento venga elaborato esclusivamente una sola volta, secondo lo schema seguente:

  • Viene creato un cestino della spesa a cui viene assegnato un ID di cestino unico.
  • Se un utente fa click sul pulsante Back/Indietro dopo aver aggiunto un elemento al cestino, il browser ricarica dal server le informazioni aggiornate relative al cestino mostrando all'utente che l'articolo è ancora nel cestino. È l'utente che aggiunge eventualmente un altro articolo identico.
  • Quando viene inviato il cestino, il suo contenuto sarà "scaricato" su un sottosistema di acquisto; a questo punto, il cestino viene invalidato, il suo numero di transazione viene salvato in una tabella con lo storico delle operazioni, se necessario, e poi eliminato dal contesto dell'applicazione.
  • Quando un utente fa click sul pulsante Back/Indietro dopo che l'acquisto è stato effettuato, il browser cercherà di ricaricare il cestino ma non ci riuscirà, poiché il cestino, il suo ID e il suo contenuto sono già stati distrutti. Il browser mostrerà un messaggio di errore, invece del cestino: inviare due volte il medesimo cestino è impossibile.
  • In caso di un browser o di un proxy che effettuano il caching, un tente che ha fatto click sul pulsante Back/Indietro vedrebbe lo stesso cestino che è stato appena inviato al sottosistema di avquisto. Il tentativo dell'utente di reinviare nuovamente il cestino fallirà, poiché l'ID di individuazione del cestino è già stato distrutto insieme al cestino stesso. Un modo cortese con cui il server può presentare tale situazione agli utenti di browser che effettuano il caching è di mostrare un errore che spieghi che il cestino già inviato non esiste più.

Il mantra
Il pattern PRG può essere riformulato come segue:

  • Mai mostrare pagine in response a POST
  • Caricare pagine usando sempre GET
  • Muoversi da POST a GET usando REDIRECT
  • Ripetere questi versi prima di andare a dormire.
  • Pensare in termini di risorse

Le applicazioni di tipo desktop sono incentrate sulla presentazione. Quando si seleziona una voce del menu, più o meno si sa già che tipo di finestra sarà mostrata e l'aspetto che avrà. A seconda dello stato del Modello, potrà mostrare informazioni diverse, ma l'aspetto generale della finestra sarà lo stesso. Le interfacce utenti delle applicazioni locali di tipo desktop sono relativamente statiche e in gran parte vengono definite nella fase di sviluppo.
Le web application dovrebbero essere incentrate sulle risorse. Possono puntare a una maggiore flessibilità di presentazione, invece di sviluppare in modo fisso una particolare pagina. I browser dovrebbero effettuare la richiesta di una risorsa da un server, ovvero di una entità dell'applicazione, non di una pagina. A seconda della disponibilità e dello stato della risorsa, il server genererà una presentazione diversa per tale risorsa, che potrà essere una pagina web "sola lettura", un form con controlli per l'immissione di dati, o un messaggio per comunicare che la risorsa non è disponibile o che è stata rimossa in maniera permanente. Occore pensare in termini di risorse, non di pagine.

Lavorare con gli oggetti
Quando si ottengono dati in input, occorre pensare a quali oggetti tali dati appartengono. Quando si mostrano dati, occorre conoscere a quale oggetto appartiene il contenuto mostrato. In ogni momento, si deve sapere con quali oggetti si sta lavorando. Occorre usare gli ID di oggetto per caricare, mostrare e salvare gli oggetti. Passare gli ID di ogetto come parametri delle request.
È bene usare la sessione, o altri sistemi di immagazzinamento lato server a breve termine, come buffer per gli oggetti che in quel momento vengono modificati o visualizzati. Assicurarsi che le proprie Viste rappresentino sempre lo stato corrente del Modello.

 

Proteggere il Model
Una pagina web è solamente un involucro per quello che gli sta dietro: il Model, gli oggetti appartenenti all'applicazione, il database. Quel che viene mostrato a un utente è importante, ma ancor più importante è quello che è salvato nel Model. Proteggete il Model, consolidatelo, costruitegli intorno tutti i possibili sistemi di gestione degli errori. Dopo tutto, un'interfaccia utente inconsistente è un fastidio rimediabile; il caos comincia quando va all'aria il modello.
Accesso e aggiornamento del Model dovrebbero avvenire secondo poche modalità ben definite. In generale, il Modello non dovrebbe consentire aggiornamenti concorrenti da parte del medesimo utente. Mantenere il Model valido e aggiornato è la garanzia più sicura per difendersi dalle incoerenze tra gli strati di presentazione e quelli di elaborazione e dipersistenza.
Occorre definire regole di business/persistenza molto chiare e non fare affidamento sullo strato web per validare i dati in input. I dati possono provenire da qualsiasi origine: da un utente di una pagina web, da un web service, da un'applicazione di terze parti o anche dagli extraterrestri, ma non si deve fare cieco affidamento su essi: i dati in input vanno validati direttamente nel cuore dell'applicazione, nel Model.

 

Evitare i doppi invii
Includete l'ID dell'oggetto e i dati relativi al momento della modifica (timestamp) nelle pagine con i form, fornite il tempo di modifica per tutti gli oggetti business nel model. Usate gli ID per effettuare la lookup del business object nel meccanismo di salvataggio persistente e anche i timestamp per riconoscere un doppio invio effettuato da una pagina presente in cache.
È possibile prendere in considerazione l'applicabilità dei token. Un token consente di individuare un doppio invio a partire da una pagina scaduta contenente un form. Il token viene salvato nella sessione prima che il form venga inviato per la prima volta; lo stesso valore del token viene inserito nel form http. Quando il form viene inviato, con esso viene inviato anche il valore del token. L'applicazione verifica che il token sia presente nella sessione, accetta i dati in input e rimuove quel token dalla sessione. Se un form non più "vivo" viene inviato nuovamente, il token del form non troverà più la sua controparte nella sessione. I token possono essere usati come soluzione puramente di strato web; Struts include già il supporto per i token.
Il model può gestire i doppi invii in maniera più affidabile rispetto ai token. Se un form viene utilizzato per aggiungere o eliminare dati, meglio applicare i valori di input direttamente al Model: se è realizzato in maniera corretta, il modello lancerà un'eccezione di inserimento o di cancellazione. Se un form è usato per la modifica di oggetti esistenti, è bene confrontare il timestamp del form con quello del database, e non accettare input con un timestamp precedente a quello dei dati già resi persistenti.
Controllare i dati con il Model rende tutto più semplice; è possibile notificare a un utente che i dati appena reinviati sono già presenti nel database: "Grazie. Per favore smetti di fare click sul pulsante e di ricaricare la pagina. Il form di input originario è già stato elaborato, ma il tuo browser ancora lo mantiene nella cache".
Impedire il caching di pagine dell'applicazione, inserendo

<meta HTTP-EQUIV="Pragma" content="no-cache">

e

<meta HTTP-EQUIV="Expires" content="-1">

nelle proprie pagine. Una pagina sarà considerata scaduta appena caricata dal server.
Separare l'input dall'output
È meglio usare classi diverse per elaborare l'input e l'output. Se si usa Struts, creare delle classi separate per i form di input e output, il che funziona molto bene con il pattern PRG a due fasi:

  • La request POST è ricevuta dal server;
  • Struts popola la classe del form di input con i parametri della request;
  • la classe del form di input valida i dati immessi;
  • il Model viene aggiornato e le informazioni connesse all'operazione corrente vengono salvate nella sessione per l'uso con le request GET che seguiranno;
  • il browser viene rediretto all'action di output e carica la pagina risultante utilizzando GET;
  • il server ricerca l'oggetto corrente nella sessione e/o nel modello e riempie il form di output;
  • viene creata una vista a partire dai dati presenti nel form di output e tale presentazione viene inviata al browser.

Nel form di input è possibile definire solo metodi di impostazione (set), mentre nel form di output vengono definiti solo metodi di recupero (get): ciò rende le classi form più facili da leggere e assicura che Struts non popolerà i campi del form di output con parametri della request.
Sarà il caso di suddividere anche le grandi classi di action in actiondi input e di output.
Usare oggetti dell'interfaccia utente in visibilità di sessione
Il pattern PRG un percorso che riporta al browser, in cui i dati della request vanno perduti. Esistono due possibilità per conservare i dati inviati con il POST: tasferirili su una response di redirezione e poi in una request GET, oppure salvarli sul server. Il primo approccio è pesante e non privo di inconvenienti poiché ci si ritroverebbe con due tipi diversi di request GET: una per mostrare nuovamente il form HTML con tutti i suoi dati precedenti e un'altra per mostrare l'oggetto applicativo proveniente dal database che usa solo il suo ID.
Pertanto, il modo corretto risulta quello di salvare dati temporanei sul server e fornire solo la request GET con l'ID. In tal modo, l'azione di output non sarà neanche a conoscenza del fatto se l'oggetto sia stato appena creato o caricato a partire dal database.
I dati temporanei corrispondono all'oggetto che al momento è modificato o visualizzato, e comprendono sia i dati dello strato business che quelli dello strato di presentazione, come:

  • valore dell'oggetto;
  • messaggi di errore inerenti il determinato oggetto;
  • titolo della pagina.

Dal momento che questo oggetto temporaneo definisce la presentazione di un oggetto business, lo denomino "oggetto UI" (User Interface, "interfaccia utente"). Con Struts, è possibile usare come oggetti UI le classi di form in scope di sessione: si tratta del modo più semplice per trasformare una normale applicazione di "inoltro" in una di "redirezione" one.
A quanto pare, la stessa classe di form sarebbe utilizzata sia nelle azioni di input che in quelle di outpu, pertanto l'action di output potrebbe accedere a valori impostati nella fase di input. Il ricorso alle classi di form in scope di sessione viene quindi inficiato dal fatto che Struts effettua un ripopolamento dei campi del form con ciascuna request. Ciò non è desiderabile, pertanto gli operatori di modifica dovranno necessariamente verificare il nome delle corrispondenze con l'azione corrente e non dovranno aggiornare i valori dei campi per l'azione di output. Struts invoca il metodo reset prima di popolare il form e validate dopo questo. Se tali metodi vengono usati sia in input che in output, il nome della corrispondenza corrente dovrebbe essere verificato, in maniera che possa essere utilizzato il codice opportuno.
Le classi di form in scope di sessione vengono mantenute in memoria durante la sessione del client, il che può trasformarsi in un problema. Se la classe del nostro form ha riferimenti a grandi oggetti, potrà essere necessario rilasciare questi oggetti in maniera manuale.
Un altro problema sorge quando deve essere creata più di un'istanza del form: come è possibile farlo a partire dal codice dell'applicazione se le classi di form vengono gestite da Struts?
Quindi, nonostante la sicura comodità di classi di form in scope di sessione, suggerisco di creare i propri oggetti UI. È possibile avere un migliore controllo su di essi e decidere se vogliamo salvarli nella sessione o nel database, con una conseguente maggiore astrazione dal framework Struts e un più facile porting verso altri framework.
Le classi di form sono pensate per due semplici obiettivi: recapitare i dati input immessi in un form HTML e visualizzare i dati output su una pagina web. Le classi form sono solo oggetti di valori, migliorati grazie a funzionalità aggiuntive quali la validazione. Meglio mantenerle in scope di request e non usarle per salvare UI o dati business.
Struts: usare la classe ForwardAction nelle azioni di output
Se è stata effettuata la separazione tra classi form di input e di output, ci si ritroverà con due serie di metodi di reset e di validazione. Tali metodi vengono invocati da Struts prima di passare il controllo a una classe action. È possibile usare i metodi validate nel form di input per il loro scopo originario: verificare i dati di input. Il form di output, dal canto suo, non ha molto da validare, visto che è usato sostanzialmente per costruire la pagina di risultato. Pertanto è possibilespostare il codice dal metodo execute della classe action al metodo validate della classe form di output e sbarazzarsi della classe dell'action custom.

 

Struts: non esporre le View
Le viste, che sono generalmente delle pagine JSP, non devono essere disponibili per un acesso diretto dal browser. Scordatevi del fatto che JSP può elaborare la request e considerate le JSP come HTML con accesso ai dati, da usare solamente per la presentazione dell'output. Occorre sempre passare il controllo attraverso la classe dell'action e/o del form. Ciò assicura una netta separazione tra le diverse componenti e consente al Controller di controllare tutte le request. Meglio nascondere le pagine web nella directory WEB-INF e mostrarle solo a partire dalle loro rispettive action.
Configurare la cache
Ai browser non viene richiesto di elaborare i tag di controllo della cache presenti nelle pagine web, ma obbediscono in genere ai campi degli header HTTP della response. Aggiungete <controller nocache="true"/> al file struts-config.xml: Struts modificherà l'header di ciascuna response nel modo seguente:

response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 1);

I corrispondenti campi dell'header http prodotti da Tomcat 4.0.6 hanno il seguente aspetto:

"Pragma: No-cache"
"Cache-Control: no-cache"
"Expires: Thu, 01 Jan 1970 00:00:00 GMT"


Usare browser migliori
Nonostante gli sforzi per proibire il caching, alcuni browser web come Firefox non ne tengono conto. Salvare le pagine in cache funziona alla grande con le semplici applicazioni di forward con cui impedisce i doppi invii impliciti. Ma salvare nella cache una pagina che si suppone rifletta lo stato corrente del server rovina l'usabilità da parte dell'utente e introduce nuovamente il problema del doppio invio.
Altri browser quali Opera possono reinviare request POST senza presentare messaggi di conferma. Ciò può invalidare lo stato dell'applicazione che non effettua il controllo sul doppio invio, e un utente non se ne accorgerebbe nemmeno.
Il vecchio Netscape Navigator funziona bene, secondo me, ma per qualche ragione si blocca per alcuni secondi quando sta inviando una request POST su un server Tomcat. Altri browser non mostrano tale strano comportamento.
Internet Explorer si comporta nel modo giusto quasi in tutto, ma è piuttosto fastidioso nei messaggi: quando si renvia nuovamente una request POST, mostra una finestra in cui comunica che la pagina è scaduta e poi una finestra di dialogo che chiede se si deve continuare. Nel caso si decida di non farlo, la nostra attuale pagina va perduta. Ma, dal momento che l'applicazione non soffrirà inconvenienti da doppio invio, i nostri clienti non avranno problemi se non questi fastidiosi messaggi.

 

Perché funziona redirect
È interessante il fatto che il pattern PRG sfrutti un comportamento non standard dei browser e dei server web. L'HTTP 1.1 definisce svariati codici di redirezione della response con i numeri 3xx. Alcuni di questi codici richiedono che il browser usi lo stesso tipo di request, altri richiedono di cambiare il POST in GET, altri ancora richiedono di ottenere una conferma da parrte dell'utente. Emerge però che molti di questi requisiti non sono implementati da parte dei browser più diffusi, i quali hanno invece dei comportamenti comuni ormai acquisiti de facto, quali la redirezione del POST in GET senza richiedere conferme nel caso venga ricevuto un codice 302. Questa è la caratteristica sfruttata dal pattern PRG.
Questo comportamente sarebbe sbagliato per il codice 302 ("Found") ma è assolutamente corretto per il codice 303 ("See Other"). Eppure, pochissimi server restituiscono un codice 303 quando sarebbe richiesta una redirezione con il metodo GET. Il metodo HttpResponse.sendRedirect non consente di impostare codici di response, ma restituisce sempre il 302. È possibile emulare il comportamento di sendRedirect(url) usando i seguenti metodi:

res.setStatus(res.SC_SEE_OTHER);
res.setHeader("Location",url);

dove SC_SEE_OTHER è il corretto codice 303, ma sendRedirect fornisce alcuni ulteriori servizi quali la risoluzione degli indirizzi relativi, quindi non si tratta proprio di una soluzione semplice e diretta. La discrepanza tra il comportamento del browser e lo standard HTTP può essere risolto se i codici 302 e 303 fossero considerati come uguali, ma venisse anche creato un nuovo codice per il comportamento standard 302.
In ogni caso, dubito che i produttori di browser cambieranno l'implementazione del codice di response 302, poiché troppe applicazioni fanno affidamento su di esso. L'aspetto positivo è che i browser più moderni capiscono ed elaborano correttamente il codice 303, pertanto, se proprio si vuole stare al sicuro, è meglio restituire il 303 invece che il 302.

 

Riferimenti
"GET after POST" di Adam Vandenberg:
http://theflangynews.editthispage.com/stories/storyReader$1118

"A Fast Introduction to Basic Servlet Programming" d Marty Hall:
http://www.informit.com/articles/article.asp?p=29817&seqNum=7

"Redirect in response to POST transaction" di A.J.Flavell:
http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html

"Post/Redirect/Get pattern for web applications" di Michael Jouravlev:
http://www.theserverside.com/patterns/thread.tss?thread_id=20936

"So, You Don't Want To Cache, Huh?" d Joe Burns:
http://www.htmlgoodies.com/beyond/nocache.html

RFC 1945, Hypertext Transfer Protocol -- HTTP/1.0 di T. Berners-Lee, R. Fielding, H. Frystyk:
http://www.ietf.org/rfc/rfc1945.txt

RFC 2616, Hypertext Transfer Protocol -- HTTP/1.1 di R. Fielding, J. Gettys, J. Mogul, H. Frystyk, L. Masinter, P. Leach, T. Berners-Lee:
http://www.w3.org/Protocols/rfc2616/rfc2616.html

 

L'autore
Michael Jouravlev è laureato in Informatica presso l'università dell'Aeronautica di Mosca, Russia e ha più di 10 anni di esperienza nello sviluppo di applicazioni sulle piattaforme MS-DOS, Windows e Java, e si è dedicato negli ultimi cinque anni principalmente ad applicazioni lato server in Java. Attualmente è impiegato come ingegnere del software press la International Lottery and Totalizator, Inc., www.ilts.com