In un progetto recente abbiamo implementato la possibilità per gli utenti di caricare file di grandi dimensioni (circa 20 GB) nel sistema tramite un’applicazione Web. Considerati il deployment e l’uso previsti del sistema, abbiamo ipotizzato che gli utenti potessero avere una connessione di rete relativamente lenta o instabile. Il caricamento di questi file poteva quindi richiedere molti minuti o addirittura ore. Volevamo assicurarci che l’utente non perdesse i propri progressi in caso di interruzioni di rete durante il processo.
Per supportare la possibilità di riprendere un upload interrotto abbiamo adottato il protocollo tus, che sta rapidamente diventando lo standard de facto in questo ambito. Ci sembra una buona occasione per condividere la nostra esperienza, con qualche suggerimento per chi deve usarlo e testarlo.
Il protocollo tus
Il protocollo tus è stato progettato come una specifica semplice per implementare upload di file riprendibili: un processo di caricamento in cui il client può riprendere in un secondo momento un upload interrotto (per esempio perché ha perso la connessione a un certo punto del caricamento) senza perdere i progressi fatti. Esistono altre soluzioni (p.es. Resumable.js), ma tus è ampiamente adottato: per esempio da Cloudflare, Supabase e Vimeo. È stato introdotto dal team di Transloadit e la sua specifica, insieme alle implementazioni di riferimento, è open source con licenza MIT. L’IETF sta inoltre standardizzando parte del protocollo nella bozza Internet “Resumable Uploads for HTTP”.
L’attuale protocollo tus 1.0 è composto da un nucleo molto ridotto più una serie di estensioni.
Il nucleo prevede due operazioni principali per lavorare con le risorse che rappresentano gli upload in corso:
- Il client può inviare una richiesta HEAD all’URL di una risorsa per recuperare l’offset corrente in byte, che indica quanti byte il server ha ricevuto fino a quel momento per l’upload.
- Il client può inviare una richiesta PATCH per caricare ulteriori dati per una risorsa.
Il protocollo di base non specifica come queste risorse vengano create sul server, perché può variare tra le diverse implementazioni. L’estensione Creation, se implementata, consente ai client di creare nuove risorse di upload con una richiesta POST.

Altre estensioni opzionali del protocollo includono:
- Creation with Upload consente al client di caricare parte del file o l’intero file nella richiesta di creazione iniziale;
- Expiration consente al server di restituire header che indicano che i dati parziali degli upload non completati verranno eliminati dopo un certo tempo;
- Checksum consente a client e server di usare checksum per verificare l’integrità dei dati caricati in ogni richiesta PATCH;
- Termination consente al client di segnalare al server che un upload non completato non verrà ripreso, così che il server possa eliminare i dati parziali;
- Concatenation consente al client di caricare in parallelo più chunk dello stesso file, che il server concatenerà al completamento dell’upload.
Va notato che tus non richiede che i file vengano caricati in chunk di dimensione fissa. Il client può infatti tentare di caricare l’intero file in una singola richiesta PATCH (o nella richiesta POST iniziale, usando l’estensione Creation with Upload). Se la richiesta fallisce in qualsiasi momento, il client può inviare successivamente una richiesta HEAD per recuperare l’offset fino al quale il server ha ricevuto e memorizzato i dati. Questo presuppone, naturalmente, che il server inizi a scrivere i dati man mano che li riceve, senza attendere l’intero payload. Il client riprende poi l’upload da quell’offset con una richiesta PATCH. Questo approccio senza chunk fissi è raccomandato dall’implementazione tus-js-client come opzione con le migliori prestazioni di upload (usare chunk fissi implica più richieste e quindi più overhead, probabilmente trascurabile per chunk dell’ordine di decine di MB).
Per una descrizione completa del protocollo, consulta la specifica ufficiale.
Implementazioni di tus
Il sito di tus elenca diverse implementazioni dei lati server e client del protocollo, sia ufficiali (sviluppate dagli autori del protocollo) sia di terze parti.
Nel nostro caso stavamo sviluppando un’applicazione Web con frontend TypeScript/React/Next.js e backend in Java con Spring Boot. Abbiamo usato l’implementazione ufficiale tus-js-client nel frontend (lato client nel browser, non nella parte server di Next.js) e la libreria di terze parti tus-java-server nel backend.
Entrambe le librerie ci sono sembrate ben documentate, facili da usare e affidabili nel nostro caso d’uso.
Inizialmente avevamo qualche dubbio su tus-java-server, una libreria di terze parti senza attività recente e che non sembra molto diffusa. Non abbiamo però riscontrato alcun problema nell’usarla. La libreria è inoltre abbastanza semplice e ben strutturata da rendere facile un fork e un’eventuale modifica, se necessario. Come alternativa per il backend avevamo considerato tusd, l’implementazione di riferimento ufficiale in Go, che può essere distribuita come servizio standalone. Farla funzionare con il nostro backend per integrare il caricamento dei file (gestito da tusd) con validazione e memorizzazione dei metadati (gestite dal nostro backend), pur essendo probabilmente fattibile con gli hook, avrebbe però complicato architettura e flusso di upload.
Implementazione backend
Il servizio di upload dei file con tus viene inizializzato così:
new TusFileUploadService()
.withUploadUri(/* URI base per gli upload, p.es. "/files" */)
.withStoragePath(/* percorso dove salvare upload e metadati */)
.withUploadExpirationPeriod(/* millisecondi dopo i quali scade un upload non completato */);
Così si usa lo storage predefinito, che salva gli upload (compresi quelli incompleti) e i metadati nel file system.
Questo è il succo della gestione di una richiesta tus:
@RequestMapping(
value = {/* URI base per gli upload */, /* URI base per gli upload */ + "/**"},
method = {RequestMethod.POST, RequestMethod.PATCH, RequestMethod.HEAD, RequestMethod.DELETE}
)
public void upload(
jakarta.servlet.http.HttpServletRequest servletRequest,
jakarta.servlet.http.HttpServletResponse servletResponse
) {
String requestURI = servletRequest.getRequestURI();
tusFileUploadService.process(servletRequest, servletResponse);
// Dopo aver elaborato la richiesta, controlla se l'upload è completato
UploadInfo uploadInfo = tusFileUploadService.getUploadInfo(requestURI);
if (uploadInfo == null || uploadInfo.isUploadInProgress()) {
return;
}
// Se è completato, fai quello che ci dovevi fare
try {
processCompletedUpload(uploadInfo, requestURI);
} finally {
// Rimuovi i dati dell'upload dopo che è completato
tusFileUploadService.deleteUpload(requestURI);
}
}
Se il client non completa mai un upload, tus-java-server lo considererà scaduto dopo il periodo di scadenza configurabile. Conviene chiamare tusFileUploadService.cleanup() in un task di manutenzione periodico, per eliminare i dati scaduti e tenere sotto controllo lo spazio occupato.
Implementazione frontend
La documentazione di tus-js-client fornisce un buon esempio di come avviare e riprendere un upload. La libreria include anche il supporto per retry automatici, dopo un intervallo di attesa, in caso di errore di rete.
Va notato che, per impostazione predefinita, il client memorizza gli upload avviati nel local storage, incluso un fingerprint del file; questo storage può essere consultato quando si riavvia un upload. Questo permette agli utenti di riprendere gli upload anche dopo aver chiuso la tab o riavviato il browser, ma richiede una UX adeguata per essere comprensibile. Nel caso più semplice, in cui la ripresa è supportata solo se l’utente ha ancora la tab aperta (mostrando per esempio la barra di avanzamento, il messaggio di errore di rete e un invito a riprendere), non è necessario e lo storage può essere disattivato impostando storeFingerprintForResuming a false.
Testare gli upload riprendibili
Abbiamo incontrato qualche difficoltà aggiuntiva nel verificare che la ripresa degli upload funzionasse correttamente, sia manualmente sia nei test automatizzati con Playwright. Ecco quindi un paio di suggerimenti.
Per testarlo dovevamo simulare il browser che va offline durante il caricamento. Anche nei test manuali, simulare questo scenario è chiaramente meno scomodo che mettere realmente offline il browser (per esempio disattivando il Wi-Fi).
L’opzione più semplice sarebbe mettere il browser in modalità offline, per esempio tramite le funzioni di throttling dei DevTools di Google Chrome (Chrome era il browser principale da supportare). Si può fare anche in Playwright, che sotto il cofano usa la stessa funzionalità.
Si è però rivelato che questo non funziona come previsto. Abbiamo provato a simulare un upload di lunga durata impostando il throttling a una velocità bassa (per esempio 3G) e poi su “Offline” per simulare la disconnessione. Sembra che, quando si passa il throttling a Offline, Chrome completi la richiesta in sospeso (senza applicarvi il throttling) prima di andare offline. Non abbiamo trovato documentazione a riguardo, ma abbiamo osservato sistematicamente questo comportamento.

Una soluzione è simulare la disconnessione non attraverso il browser, ma aggiungendo un proxy che si possa disattivare facilmente. Per esempio (supponendo che frontend e backend siano serviti localmente sulla porta 3000), si esegue socat tcp-listen:3001,fork,reuseaddr tcp:localhost:3000 per avviare un processo in ascolto sulla porta 3001 che fa da proxy verso la porta 3000. Si apre l’applicazione web sulla porta 3001, si avvia un upload e poi si termina il processo socat. Il browser non riuscirà più a raggiungere il server tus (perché sta cercando di contattarlo sulla porta 3001, su cui non c’è più il proxy), quindi il client tus dovrebbe segnalare un errore e poi, presumibilmente, tentare di riprendere automaticamente o chiedere all’utente di riprendere.
Se l’applicazione include già un reverse proxy (per esempio per verificare l’autenticazione), si può disattivare quello invece di usare un proxy separato con socat.
Lock degli upload in tus-java-server
Un limite di questo approccio è che il server sembra reagire istantaneamente alla connessione perduta. Se il client va realmente offline, invece, il server non se ne accorge subito: semplicemente smette di ricevere nuovi pacchetti.
Questo fa la differenza per tus-java-server, che crea file di lock per gli upload in corso (per evitare upload paralleli sulla stessa risorsa) e li elimina quando la connessione viene chiusa. Nel primo caso eliminerà immediatamente il file di lock; nel secondo lo eliminerà solo dopo che il server Tomcat chiude la connessione allo scadere di un timeout.
Questo timeout è controllato da server.tomcat.connection-timeout, che tra le altre cose imposta il tempo massimo tra la ricezione dei pacchetti TCP in una richiesta POST (più dettagli qui e qui). Il timeout predefinito di 60 secondi era troppo lungo per le nostre esigenze, perché volevamo che il client tentasse di riprendere automaticamente in meno di un minuto.
Quando si testa questo comportamento dei lock, quindi, il proxy con socat non equivale a disconnettere realmente il client.
Conclusioni
Il protocollo tus e le librerie che abbiamo scelto si sono rivelati ottimi per implementare gli upload riprendibili nella nostra applicazione, e sarebbero probabilmente adatti anche ad altre applicazioni con uno stack tecnologico simile che devono supportare il caricamento di file di grandi dimensioni. Il protocollo in sé è semplice, ma speriamo che questi suggerimenti su sviluppo e test possano essere utili a chi sta implementando questa funzionalità.