Resumable (and potentially asynchronous) file uploads for any Java web application, with support for both the tus 1.0.0 protocol and the new IETF Resumable Uploads for HTTP (RUFH) standard.
This library lets the users of your application upload large files over slow and unreliable connections: an upload can be paused or resumed (after a connection loss or reset) without starting over. It implements the server side of the tus v1.0.0 protocol and the official IETF Resumable Uploads for HTTP specification (draft-ietf-httpbis-resumable-upload, draft-12) on the same endpoint, so tus clients (Uppy, tus-js-client) and IETF RUFH clients work side by side.
Javadoc: https://tus.desair.me/ · Requires Java 17+ (Jakarta Servlet 6.0) · Upgrading from 1.x? See the Upgrade Guide.
- ⚡ IETF RUFH Ready: Implements the official IETF Resumable Uploads for HTTP (draft-12) — including
Upload-Complete,Upload-Limit, RFC 7807 problem details, RFC 9530 digests and104interim responses — alongside full Tus 1.0.0 support. Protocol is auto-detected per request. (RUFH is still an IETF draft, so treat that part as experimental until it becomes an RFC.) - ☁️ Pluggable Storage Backends: Local Disk Volumes, NFS network shares, S3-compatible Object Storage (AWS S3, MinIO, Cloudflare R2, Ceph, GCS), and Azure Blob Storage.
- 🔒 Zero-Database Distributed Locking: Built-in lease-based locking enabling multi-replica cluster & Kubernetes container deployments without requiring Redis or relational databases.
- 🛡️ Data Integrity & Resiliency: Built-in HTTP Digests (RFC 9530), checksum verification, and duplicate upload deduplication.
- 🚀 Production-Ready & Lightweight: Minimal dependencies (Jakarta Servlet API 6.0 & Apache Commons), non-blocking lock contention resolution, and thread-local caching.
- Quick Start
- Try It with curl
- How It Works
- Documentation
- Compatible Clients & Conformity Testing
- Versioning
- Contributing
Maven:
<dependency>
<groupId>me.desair.tus</groupId>
<artifactId>tus-java-server</artifactId>
<version>2.0.0</version>
</dependency>Gradle (Groovy):
implementation 'me.desair.tus:tus-java-server:2.0.0'Gradle (Kotlin):
implementation("me.desair.tus:tus-java-server:2.0.0")S3 and Azure backends need an additional SDK dependency, see the Configuration Reference.
The main entry point is me.desair.tus.server.TusFileUploadService.process(HttpServletRequest, HttpServletResponse). Call it from any HttpServlet, Filter or REST controller that gives you the servlet request and response. Create the service once (it is thread-safe) and share it. By default uploads are stored on a (shared) file volume.
Spring Boot 3 (spring-boot-starter-web, Java 17+). The snippets are complete and compile as-is; adjust the paths:
import me.desair.tus.server.TusFileUploadService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TusConfig {
@Bean(destroyMethod = "close")
public TusFileUploadService tusFileUploadService() {
return new TusFileUploadService()
.withStoragePath("/path/to/uploads")
.withUploadUri("/api/upload")
.withUploadDeduplication(true)
.withThreadLocalCache(true);
}
}import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import me.desair.tus.server.TusFileUploadService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FileUploadController {
private final TusFileUploadService tusFileUploadService;
public FileUploadController(TusFileUploadService tusFileUploadService) {
this.tusFileUploadService = tusFileUploadService;
}
@RequestMapping(
value = {"/api/upload", "/api/upload/**"},
method = {
RequestMethod.POST,
RequestMethod.PUT,
RequestMethod.PATCH,
RequestMethod.HEAD,
RequestMethod.DELETE,
RequestMethod.OPTIONS,
RequestMethod.GET
})
public void processUpload(HttpServletRequest request, HttpServletResponse response)
throws IOException {
tusFileUploadService.process(request, response);
}
}Plain Jakarta Servlet 6 (Tomcat 10+, Jetty 12, Undertow...)
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import me.desair.tus.server.TusFileUploadService;
@WebServlet("/api/upload/*")
public class UploadServlet extends HttpServlet {
private TusFileUploadService tus;
@Override
public void init() {
tus =
new TusFileUploadService()
.withStoragePath("/path/to/uploads")
.withUploadUri("/api/upload")
.withUploadDeduplication(true)
.withThreadLocalCache(true);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// Override service() (not doPost/doGet) so that PATCH, HEAD and OPTIONS reach the library
tus.process(req, resp);
}
@Override
public void destroy() {
try {
tus.close();
} catch (IOException e) {
log("Failed to close TusFileUploadService", e);
}
}
}CORS and OPTIONS preflight are handled by the library itself. More variants (multi-tenancy, cleanup, security) are in the Integration Guide.
Note
When using Spring Security, the upload endpoint must be authenticated like any other endpoint, but OPTIONS preflight requests must be allowed through. If your Spring Boot setup uses a /api prefix or context path, make sure withUploadUri(...) matches the path as seen by the application.
Register an UploadCompletionListener on the service from step 2 (Spring Boot version shown, replace the TusConfig class):
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import me.desair.tus.server.TusFileUploadService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TusConfig {
private static final Logger log = LoggerFactory.getLogger(TusConfig.class);
@Bean(destroyMethod = "close")
public TusFileUploadService tusFileUploadService() {
return new TusFileUploadService()
.withStoragePath("/path/to/uploads")
.withUploadUri("/api/upload")
.withUploadDeduplication(true)
.withThreadLocalCache(true)
.withUploadCompletionListener(
(uploadInfo, service) -> {
// Safely retrieve or guess the file name (preventing path traversal and NPE)
String fileName = Paths.get(uploadInfo.getFileName()).getFileName().toString();
try {
try (InputStream is = service.getUploadedBytes(uploadInfo)) {
Files.copy(is, Paths.get("/var/data/documents", fileName));
} // closing the stream releases the lock, which deleteUpload needs
service.deleteUpload(uploadInfo);
} catch (Exception e) {
log.error("Failed to process completed upload {}", uploadInfo.getId(), e);
}
});
}
}Alternatively let the client notify your own REST endpoint and call getUploadInfo, getUploadedBytes and deleteUpload there. Also schedule tusFileUploadService.cleanup() to remove expired uploads. See Handling Upload Completion and Cleanup.
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
const uppy = new Uppy().use(Tus, {
endpoint: 'http://localhost:8080/api/upload',
chunkSize: 5 * 1024 * 1024 // 5MB chunk size
});Complete working examples:
- Resumable and asynchronous file upload in Spring Boot REST API with Uppy JavaScript client.
- Detailed blog post by Ralph on how to use this library in Spring Boot in combination with the Tus JavaScript client.
- Resumable and asynchronous file upload using Uppy with form submission in Dropwizard (Jetty)
The scripts/ folder contains runnable Python test clients for both protocols (Conformity Testing Guide).
Verify your endpoint without a frontend (replace the URL and use the Location header returned by the POST).
IETF Resumable Uploads (RUFH)
# 1. Create the upload resource (more data will follow)
curl -i -X POST http://localhost:8080/api/upload -H "Upload-Complete: ?0"
# 2. Send the data and mark the upload as complete
curl -i -X PATCH http://localhost:8080/api/upload/<id> \
-H "Upload-Offset: 0" -H "Upload-Complete: ?1" \
-H "Content-Type: application/partial-upload" --data-binary "hello world"
# 3. Query progress at any time
curl -I http://localhost:8080/api/upload/<id>Tus 1.0.0
curl -i -X POST http://localhost:8080/api/upload \
-H "Tus-Resumable: 1.0.0" -H "Upload-Length: 11" -H "Upload-Metadata: filename aGVsbG8udHh0"
curl -i -X PATCH http://localhost:8080/api/upload/<id> \
-H "Tus-Resumable: 1.0.0" -H "Upload-Offset: 0" \
-H "Content-Type: application/offset+octet-stream" --data-binary "hello world"
curl -I http://localhost:8080/api/upload/<id> -H "Tus-Resumable: 1.0.0"Your application only forwards HTTP requests to TusFileUploadService; the library implements the protocol, stores the bytes in a pluggable backend, locks uploads against concurrent writers and resolves lock contention when a client resumes. Your application consumes the finished upload and deletes it.
sequenceDiagram
autonumber
actor Client as Client (Uppy / tus-js-client / RUFH client)
participant Protocol as Upload endpoint (/api/upload)
participant Storage as Storage (Disk / S3 / Azure)
participant App as Your application API
Client->>Protocol: POST (create upload)
Protocol->>Storage: Initialize upload
Protocol-->>Client: 201 Created (Location)
Client->>Protocol: PATCH chunk(s), resumable after a failure
Protocol->>Storage: Append bytes
Protocol-->>Client: 204 (Upload-Offset)
Client->>App: "Upload finished" (or UploadCompletionListener)
App->>Storage: getUploadedBytes(...), deleteUpload(...)
Detailed Tus and RUFH flow diagrams: Protocols.
| Topic | Guide |
|---|---|
| Overview of all documents | Documentation index |
process(), completion, cleanup, multi-tenancy, security, frontend |
Integration Guide |
All with...() options, storage backend selection, dependencies |
Configuration Reference |
| Tus vs. IETF RUFH, protocol version selection, extensions, HTTP digests, 104 responses | Protocols, Migrating clients to RUFH, Interim responses |
| Multi-replica, Kubernetes, reverse proxies | Deployment Guide, Locking |
| Storage backends | Disk & network (NFS/EFS/SMB), S3-compatible, Azure Blob |
| Problems | Troubleshooting |
| Upgrading from 1.x | Upgrade Guide, CHANGELOG |
The server is tested with Uppy, tus-js-client, the built-in Tus v1.0.0 and RUFH (draft-12 and RFC 9530) conformity suites in scripts/ across Disk, S3 and Azure Blob backends, and the community RUFH conformity tests from the IETF hackathon. See Protocols and the Conformity Testing Guide.
This artifact follows MAJOR.MINOR.PATCH semantic versioning. Version 2.0.0 introduces major dual-protocol support for both Tus 1.0.0 and the IETF Resumable Uploads for HTTP specification (draft-ietf-httpbis-resumable-upload). Version 1.0.0-3.3 was the last Tus protocol-only version.
This library comes without any warranty and is released under a MIT license. Bug reports, ideas and pull requests are welcome; contributed code needs automated tests and must follow the Google Java Style. See CONTRIBUTING.md for details, code style and testing instructions.