Implementing Resumable File Upload with Tus in Spring Boot Using tus-java-server
Oct 2026 · 15 min read
A guide to implementing Tus protocol-based resumable file upload in Spring Boot using the tus-java-server library, including service configuration, servlet endpoints, storage, cleanup, metadata, and tus-js-client integration.
Large file uploads are prone to failure when the connection drops. If an application only uses a regular upload endpoint, the user has to resend the file from scratch. The Tus protocol solves this problem by storing the upload offset on the server so the client can resume sending from the last byte successfully received.
In the previous article, the POST, HEAD, and PATCH endpoints were written manually. That approach is useful for learning the basics of the protocol, but it requires a lot of extra work to handle header validation, offset conflicts, expiration, termination, checksums, locking, and spec-compliant error responses.
This article follows the actual implementation of the tus_library project, which uses Spring Boot 4.1.1, Java 21, tus-java-server 1.0.0-3.3, Thymeleaf, and tus-js-client major version 4. That Java library was also known in older versions as tus-server-lib and provides TusFileUploadService to process Tus requests through the Jakarta Servlet API.
Basic Concepts of the Tus Protocol
Tus is an HTTP-based resumable upload protocol. The server creates an upload resource, stores the position of bytes already received, and then ensures each subsequent chunk is written at the correct offset.
The basic flow:
OPTIONS: the client reads the version, extensions, and size limits supported by the server.
Creation: the client sends a
POSTwithUpload-Lengthand optional metadata. The server returns201 Createdplus aLocationheader.Head: the client sends a
HEADto the resource URL to read the latestUpload-Offset.PATCH: the client sends a chunk with
Content-Type: application/offset+octet-streamandUpload-Offset.Termination: the client can send a
DELETEto cancel and remove the resource if the termination extension is available.Expiration: abandoned uploads can be given a lifetime and cleaned up periodically.
Tus requests use headers like the following:
Tus-Resumable: 1.0.0
Upload-Length: 10485760
Upload-Offset: 5242880
Upload-Metadata: filename ZG9rdW1lbi5wZGY=,filetype YXBwbGljYXRpb24vcGRm
Unlike a manual implementation, metadata parsing, protocol validation, locking, and offset management are delegated to the library.
Dependency Choices
The project uses the Spring Boot parent 4.1.1, Java 21, and the following dependencies:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>me.desair.tus</groupId>
<artifactId>tus-java-server</artifactId>
<version>1.0.0-3.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
The tus-java-server 1.0.0-3.x series requires Java 17 or newer and uses the jakarta.servlet package. Therefore, the controller imports jakarta.servlet.http.HttpServletRequest and jakarta.servlet.http.HttpServletResponse, not the old javax.servlet package.
Spring Boot Project Setup
The source structure of the project used:
src/main/java/dev/sch39/tus_library/
├── config/
│ └── TusConfig.java
├── controller/
│ ├── TusUploadController.java
│ └── UploadPageController.java
├── maintenance/
│ └── TusUploadCleanup.java
└── TusLibraryApplication.java
src/main/resources/
├── static/js/
│ └── upload.js
├── templates/
│ └── upload.html
└── application.properties
src/test/java/dev/sch39/tus_library/
└── TusLibraryApplicationTests.java
The main class enables scheduling so the application can run cleanup periodically:
package dev.sch39.tus_library;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class TusLibraryApplication {
public static void main(String[] args) {
SpringApplication.run(TusLibraryApplication.class, args);
}
}
Tus Endpoint Configuration
The actual configuration stores uploads in a local directory, exposes the /files endpoint, and runs cleanup every five minutes:
spring.application.name=tus-library
tus.upload-uri=/files
tus.storage-path=./uploads
tus.max-upload-size=1073741824
tus.expiration-period=86400000
tus.cleanup-interval=300000
Those values mean:
tus.upload-uri=/files— the base URI accepted by the client.tus.storage-path=./uploads— the library's internal storage root, relative to the JVM working directory.tus.max-upload-size=1073741824— maximum upload size of 1 GiB.tus.expiration-period=86400000— upload lifetime of 24 hours in milliseconds.tus.cleanup-interval=300000— fixed delay for cleanup of 300,000 ms or five minutes.
withUploadUri(...) must match the path visible to the application. If the application runs behind a reverse proxy or uses a context path, routing and forwarded headers must be configured consistently.
The path ./uploads does not mean every file is directly in that folder. With the built-in filesystem backend, the visible storage layout is:
uploads/
├── locks/
└── uploads/
└── <upload-id>/
├── data
└── info
The uploads/uploads folder is not a typo. The first folder is the root from the configuration, while the second folder is the library's internal structure. The data file contains the upload bytes and info contains the UploadInfo object in Java serialization format, not JSON. This internal structure should not be treated as the application's storage API.
Service Implementation
TusFileUploadService is created once as a Spring bean:
package dev.sch39.tus_library.config;
import me.desair.tus.server.TusFileUploadService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TusConfig {
@Bean
public TusFileUploadService tusFileUploadService(
@Value("${tus.upload-uri}") String uploadUri,
@Value("${tus.storage-path}") String storagePath,
@Value("${tus.max-upload-size}") Long maxUploadSize,
@Value("${tus.expiration-period}") Long expirationPeriod) {
return new TusFileUploadService()
.withUploadUri(uploadUri)
.withStoragePath(storagePath)
.withMaxUploadSize(maxUploadSize)
.withUploadExpirationPeriod(expirationPeriod)
.withThreadLocalCache(true)
.withUploadDeduplication(true);
}
}
Purpose: Configures the Tus service that handles resource creation, offset checks, chunk writing, metadata, expiration, deduplication, and upload locking.
The configuration that is actually active in the project:
withStoragePath(...)uses the local filesystem as the storage backend.withUploadUri(...)determines the main Tus endpoint.withMaxUploadSize(...)limits the length of files that can be created.withUploadExpirationPeriod(...)determines the upload lifetime.withThreadLocalCache(true)enables per-thread caching during request processing.withUploadDeduplication(true)enables the library's built-in deduplication.
The project does not enable .withDownloadFeature(), custom storage, custom locking, owner key, completion callback, or automatic movement to permanent storage. This means completed uploads remain in the storage managed by the library.
For multi-replica applications, a relative local filesystem like this example is not enough. Storage and locking need to be designed to be consistent across replicas, for example through a shared volume or a service implementation suited to the deployment.
Controller Implementation
The controller does not implement POST, HEAD, or PATCH manually. Every mapped method is forwarded to TusFileUploadService.process(...):
package dev.sch39.tus_library.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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;
import java.io.IOException;
@RestController
public class TusUploadController {
private final TusFileUploadService tusFileUploadService;
public TusUploadController(TusFileUploadService tusFileUploadService) {
this.tusFileUploadService = tusFileUploadService;
}
@RequestMapping(value = { "/files", "/files/**" }, 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);
}
}
Purpose: Bridges Spring MVC requests with the protocol implementation provided by the library.
The mapping must include the base path and subpaths:
/files
/files/**
The base path is used for creation and discovery, while subpaths are used by the unique upload resources returned through the Location header. The OPTIONS method must reach the controller so the client can read the server's capabilities.
Although GET is mapped by the controller, download is not yet available because the service configuration does not call .withDownloadFeature(). PUT is also forwarded to the library, but the frontend in this project does not call it directly.
Upload Page Implementation
The UI page is available at exactly GET /upload; the project has no mapping or redirect for /:
package dev.sch39.tus_library.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UploadPageController {
@GetMapping("/upload")
public String uploadPage() {
return "upload";
}
}
The view name upload is rendered from src/main/resources/templates/upload.html.
File Metadata Handling
The frontend sends filename and filetype metadata. The library parses the Upload-Metadata header and provides it through UploadInfo.
Upload-Metadata: filename ZG9rdW1lbi5wZGY=,filetype YXBwbGljYXRpb24vcGRm
The project currently does not read metadata or move completed uploads. The following example is an optional next step, not part of the running implementation:
import me.desair.tus.server.upload.UploadInfo;
UploadInfo uploadInfo = tusFileUploadService.getUploadInfo(uploadUri);
if (uploadInfo != null && uploadInfo.hasMetadata()) {
String filename = uploadInfo.getMetadata().get("filename");
String filetype = uploadInfo.getMetadata().get("filetype");
}
Upload status can be checked through offset, length, and isUploadInProgress(). Complete data can be read through getUploadedBytes(uploadUri):
try (var inputStream = tusFileUploadService.getUploadedBytes(uploadUri)) {
// Copy to object storage or the application's domain storage.
}
Metadata comes from the client and must not be used directly as a path. Take the last file name and validate it according to the application's needs:
String safeFilename = java.nio.file.Paths.get(filename)
.getFileName()
.toString();
After the stream is closed and the file is successfully moved to permanent storage, the temporary resource can be deleted:
tusFileUploadService.deleteUpload(uploadUri);
Cleanup of Expired Uploads
Expiration does not clean up all resources without a maintenance call. The project runs cleanup() through the following scheduler:
package dev.sch39.tus_library.maintenance;
import me.desair.tus.server.TusFileUploadService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class TusUploadCleanup {
private static final Logger log =
LoggerFactory.getLogger(TusUploadCleanup.class);
private final TusFileUploadService tusFileUploadService;
public TusUploadCleanup(TusFileUploadService tusFileUploadService) {
this.tusFileUploadService = tusFileUploadService;
}
@Scheduled(fixedDelayString = "${tus.cleanup-interval:3600000}")
public void cleanupExpiredUploads() {
try {
tusFileUploadService.cleanup();
} catch (IOException exception) {
log.error(
"Failed to clean up expired Tus uploads",
exception
);
}
}
}
A number without a unit in fixedDelayString is read in milliseconds. The annotation fallback is 3600000 or one hour, but application.properties overrides it with 300000, so the actual implementation waits five minutes after one execution finishes before running the next cleanup.
Do not use an interval that is too aggressive. For example, a value of 120 means 120 ms, not 120 seconds. Cleanup that runs very frequently can overlap with info metadata serialization and produce warnings about reading a temporary file that is currently empty or being rewritten.
Integration with the Frontend (tus-js-client)
The actual template is a responsive upload card with a file summary, native <progress>, byte count, percentage, toned status, focus state, mobile layout, and prefers-reduced-motion support. The most important integration parts are:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>Resumable File Upload</title>
</head>
<body>
<main class="page-shell">
<section
class="upload-card"
id="uploadApp"
th:data-upload-endpoint="@{/files}"
data-upload-endpoint="/files">
<div class="file-field">
<label class="field-label" for="fileInput">Choose file</label>
<input type="file" id="fileInput">
<div class="file-summary" aria-live="polite">
<span id="fileName">No file selected</span>
<span id="fileSize">—</span>
</div>
</div>
<div class="progress-panel">
<span id="percentage">0%</span>
<progress id="progress" value="0" max="100"
aria-label="File upload progress">0%</progress>
<span id="transferred">0 B of 0 B</span>
<span>5 MiB chunk</span>
</div>
<p id="status" role="status" aria-live="polite"
aria-atomic="true" data-tone="neutral">
Choose a file to start the upload.
</p>
<div class="actions">
<button id="uploadButton" type="button" disabled>
Start upload
</button>
<button id="pauseButton" type="button" disabled>
Pause
</button>
<button id="resumeButton" type="button" disabled>
Resume
</button>
</div>
</section>
</main>
<script defer
src="https://cdn.jsdelivr.net/npm/tus-js-client@4/dist/tus.min.js">
</script>
<script defer src="/js/upload.js" th:src="@{/js/upload.js}"></script>
</body>
</html>
The full CSS remains inline in the actual template. Two important Thymeleaf expressions are:
th:data-upload-endpoint="@{/files}"produces an endpoint that takes the context path into account.th:src="@{/js/upload.js}"produces a static resource URL that also takes the context path into account.
The JavaScript is in src/main/resources/static/js/upload.js, so Spring Boot serves it through /js/upload.js. The folder src/main/resources/js does not automatically become a static resource.
UI State
The frontend uses explicit state:
const activeStates = new Set([
'checking-resume',
'uploading',
'pausing',
'paused'
]);
let upload = null;
let uploadAttempt = 0;
let state = fileInput.files.length > 0
? 'file-selected'
: 'idle';
The possible states are idle, file-selected, checking-resume, uploading, pausing, paused, completed, and failed. The renderState() function is the single place to manage buttons, file input, status, and ARIA role:
function renderState() {
const file = selectedFile();
const isActive = activeStates.has(state);
uploadButton.disabled = !file || isActive || state === 'pausing';
uploadButton.textContent = state === 'failed'
? 'Try again'
: 'Start upload';
pauseButton.disabled = state !== 'uploading';
resumeButton.disabled = state !== 'paused';
fileInput.disabled = isActive || state === 'pausing';
status.textContent = statusMessage;
status.dataset.tone = statusTone;
status.setAttribute(
'role',
statusTone === 'error' ? 'alert' : 'status'
);
updateFileSummary();
}
The file input and buttons are disabled in the appropriate states so the user cannot start multiple uploads or change the file while an operation is active.
Tus Configuration
The endpoint is read from the template's data attribute, not hard-coded in JavaScript:
const uploadApp = document.getElementById('uploadApp');
const endpoint = uploadApp?.dataset.uploadEndpoint;
Each attempt creates a local instance and an incrementing token. Old callbacks should only update the UI if the instance and token are still active:
function isCurrentUpload(candidate, attempt) {
return upload === candidate && uploadAttempt === attempt;
}
const attempt = ++uploadAttempt;
const currentUpload = new window.tus.Upload(file, {
endpoint,
retryDelays: [0, 1000, 3000, 5000],
chunkSize: 5 * 1024 * 1024,
removeFingerprintOnSuccess: true,
metadata: {
filename: file.name,
filetype: file.type || 'application/octet-stream'
},
onError(error) {
if (!isCurrentUpload(currentUpload, attempt)) {
return;
}
failUpload(
'Upload failed. Check your connection then press “Try again”.',
error
);
},
onProgress(bytesUploaded, bytesTotal) {
if (!isCurrentUpload(currentUpload, attempt)) {
return;
}
state = 'uploading';
setStatus(
'Upload is in progress. You can pause it at any time.',
'info'
);
updateProgress(bytesUploaded, bytesTotal);
renderState();
},
onSuccess() {
if (!isCurrentUpload(currentUpload, attempt)) {
return;
}
state = 'completed';
updateProgress(file.size, file.size);
setStatus(
`Upload complete. Resource stored at ${currentUpload.url}`,
'success'
);
renderState();
}
});
This configuration provides the following behavior:
Chunk size of 5 MiB.
Retries happen immediately, then after 1, 3, and 5 seconds.
Empty MIME type uses the
application/octet-streamfallback.The upload fingerprint is removed after success through
removeFingerprintOnSuccess: true.Technical errors are written to the console, while the user gets a concise recovery message.
The instance guard prevents old upload callbacks from changing the UI of a newer upload.
removeFingerprintOnSuccess makes the same file uploadable again as a new resource after the previous upload is complete, rather than being treated as a resume of an already complete resource.
Resuming Previous Uploads
Before calling start(), the frontend reads history from the browser:
try {
const previousUploads =
await currentUpload.findPreviousUploads();
if (!isCurrentUpload(currentUpload, attempt)) {
return;
}
if (previousUploads.length > 0) {
currentUpload.resumeFromPreviousUpload(previousUploads[0]);
setStatus(
'Previous upload found. Resuming from the last progress…',
'info'
);
} else {
setStatus('Starting a new upload…', 'info');
}
state = 'uploading';
renderState();
currentUpload.start();
} catch (error) {
if (isCurrentUpload(currentUpload, attempt)) {
failUpload(
'Upload history cannot be read. Press “Try again” to retry.',
error
);
}
}
findPreviousUploads() looks for fingerprints and resource URLs stored by tus-js-client in browser storage. The implementation picks the first record if there are multiple results. When resuming, the client uses the old resource and requests the latest offset from the server before continuing with chunks.
The browser does not store the File object again. After a reload, the user still has to select the same local file for the fingerprint to match. The history also depends on the origin, browser profile, and browser storage availability.
Pause and Resume on the Same Page
Pause uses abort() without termination:
state = 'pausing';
setStatus('Pausing the running request…', 'warning');
renderState();
try {
await currentUpload.abort();
if (!isCurrentUpload(currentUpload, attempt)) {
return;
}
state = 'paused';
setStatus(
'Upload paused. Press “Resume” to continue from the last offset.',
'warning'
);
renderState();
} catch (error) {
if (!isCurrentUpload(currentUpload, attempt)) {
return;
}
state = 'uploading';
setStatus(
'Upload could not be paused and is still running.',
'error'
);
console.error('Failed to pause Tus upload:', error);
renderState();
}
abort() stops the active client request, but does not send DELETE and does not remove the server resource. The Resume button calls start() on the same instance:
if (upload && state === 'paused') {
state = 'uploading';
setStatus(
'Resuming upload from the last offset…',
'info'
);
renderState();
upload.start();
}
The partial resource remains on the server so it can be resumed until it is explicitly deleted or becomes eligible for cleanup.
Testing
Run the UI through:
http://localhost:8080/upload
Check endpoint capabilities with OPTIONS:
curl -X OPTIONS http://localhost:8080/files -i
The server returns 204 No Content along with headers such as Tus-Version, Tus-Extension, Tus-Checksum-Algorithm, and Tus-Max-Size according to the library's capabilities and the configuration.
Create a new upload resource:
curl -X POST http://localhost:8080/files \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: 11" \
-H "Upload-Metadata: filename dGVzdC50eHQ=" \
-i
The creation response verified in this implementation uses 201 Created and a relative Location:
HTTP/1.1 201 Created
Location: /files/<upload-id>
Tus-Resumable: 1.0.0
Read the initial offset using that URL:
curl -I http://localhost:8080/files/<upload-id> \
-H "Tus-Resumable: 1.0.0"
The tested implementation returns status 204 No Content for HEAD, with an initial offset of 0:
HTTP/1.1 204 No Content
Upload-Length: 11
Upload-Offset: 0
Tus-Resumable: 1.0.0
Send a chunk:
curl -X PATCH http://localhost:8080/files/<upload-id> \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Offset: 0" \
-H "Content-Type: application/offset+octet-stream" \
--data-binary "hello world" \
-i
A successful response uses 204 No Content and provides the new offset:
HTTP/1.1 204 No Content
Tus-Resumable: 1.0.0
Upload-Offset: 11
The only automated test currently present is a Spring context smoke test:
@SpringBootTest
class TusLibraryApplicationTests {
@Test
void contextLoads() {
}
}
That test proves the context can be created, but does not yet test the protocol or UI. The last result ran one test without failures or errors. The following scenarios are still recommended and have not been implemented as automated tests:
OPTIONSreturns the server's capabilities.POSTproduces201andLocation.HEADreturns an initial offset of0.PATCHincreases the offset by the number of bytes.An incorrect offset is rejected with a conflict.
Uploads above
tus.max-upload-sizeare rejected.DELETEremoves the resource when termination is available.Uploads can be resumed after a restart with persistent storage.
Cleanup removes expired uploads.
GET /uploadand/js/upload.jsare accessible.The frontend start, pause, resume, retry, reload, and completed flows work in the browser.
Why It Works
Spring MVC forwards all requests on
/filesand/files/**to a singleTusFileUploadServiceinstance.The library reads and validates Tus headers based on the HTTP method received.
Metadata, file length, offset, and upload data are stored through the library's filesystem backend.
A
HEADrequest returns the last stored offset.A
PATCHrequest is only accepted if the client offset matches the server offset.Locking prevents multiple requests from writing to the same resource unsafely.
tus-js-clientstores fingerprints and resource URLs for retry or resume without resending bytes already received.UI state and the attempt guard prevent duplicate uploads and old callbacks from changing the display.
Expiration and scheduled cleanup control abandoned upload resources.
With the library, the application does not need to write an Upload-Metadata parser, manage RandomAccessFile, or compose the entire protocol response manually. The application is still responsible for authentication, authorization, business validation, moving completed files, and the permanent storage lifecycle.
Edge Cases
URI mismatch: the
withUploadUri(...)value must match the controller endpoint and the path seen by the application.Working directory:
./uploadsis relative to the JVM working directory, which can differ when the application runs as a service or container.Reverse proxy: make sure forwarded headers, scheme, host, and prefix are consistent so the
Locationcan be used by the client.Spring Security and CORS: the endpoint does not yet have security configuration; allow the necessary
OPTIONSand apply authentication consistently.Upload ownership: the controller currently calls
process(request, response)without anownerKey, so there is no resource isolation between users.Deduplication: deduplication is active while ownership has not been applied. Evaluate privacy implications before using this pattern in a multi-tenant application.
Dangerous file names: metadata comes from the client; sanitize the filename and do not use it directly as a filesystem path.
File size: the server limits to 1 GiB, but the frontend does not precheck and only shows a generic error if the server rejects the upload.
Completed files: success status only means the upload is complete in the library storage. The project has not moved it to permanent storage or removed the temporary resource.
Abandoned uploads: pause is not delete. The partial resource remains available until it is resumed, deleted, or cleaned up.
Cleanup: an interval value without a unit is milliseconds. An interval that is too small can increase the chance of cleanup overlapping with metadata writing.
Multiple replicas: local storage and default locking are not enough for a distributed deployment without appropriate shared storage.
Download:
GETis mapped, but.withDownloadFeature()is not active so the Tus URL is not the application's download endpoint.Browser resume: history depends on the origin and browser storage; after a reload the user must select the same file.
Multiple histories: the implementation directly picks
previousUploads[0]and does not offer a picker to the user.CDN: the
tus-js-client@4bundle requires an internet connection, only locks the major version, and does not yet have SRI or a local fallback.Resource URL: the UI displays the Tus URL after success; consider whether that URL should be exposed after access control is applied.
Observability: the project does not yet have metrics, tracing, audit trails, or completion callbacks.
Test coverage: the new automated test only verifies the Spring context, not protocol and frontend correctness.
Conclusion
tus-java-server reduces the complexity of implementing resumable uploads in Spring Boot. The controller only forwards servlet requests to TusFileUploadService, while the library handles creation, offset, chunks, metadata, expiration, termination, checksums, and locking according to the active capabilities and configuration.
The actual implementation also strengthens the browser side with a context-path-safe endpoint, 5 MiB chunks, retries, previous upload lookup, pause/resume, explicit UI state, asynchronous error handling, and guards against stale callbacks.
Even so, the library is not a replacement for the entire upload design of an application. Authentication, ownership, quotas, metadata and content validation, processing of completed files, observability, backup, permanent storage, and protocol tests still need to be designed according to system requirements.