java 65 lines · 10 steps

HTTP range requests in Spring

A Spring endpoint that streams files and honors byte-range requests for resumable, seekable downloads.

Explained by highlit
1@GetMapping("/files/{id}")
2public ResponseEntity<StreamingResponseBody> download(
3 @PathVariable String id,
4 @RequestHeader(value = HttpHeaders.RANGE, required = false) String rangeHeader) throws IOException {
5 
6 Path file = storage.resolve(id);
7 if (!Files.isReadable(file)) {
8 return ResponseEntity.notFound().build();
9 }
10 
11 long length = Files.size(file);
12 String contentType = Optional.ofNullable(Files.probeContentType(file))
13 .orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE);
14 
15 if (rangeHeader == null || !rangeHeader.startsWith("bytes=")) {
16 return ResponseEntity.ok()
17 .header(HttpHeaders.ACCEPT_RANGES, "bytes")
18 .contentType(MediaType.parseMediaType(contentType))
19 .contentLength(length)
20 .body(out -> Files.copy(file, out));
21 }
22 
23 String spec = rangeHeader.substring(6).split(",", 2)[0].trim();
24 int dash = spec.indexOf('-');
25 long start = dash == 0 ? -1 : Long.parseLong(spec.substring(0, dash));
26 long end = dash == spec.length() - 1 ? -1 : Long.parseLong(spec.substring(dash + 1));
27 
28 if (start == -1) {
29 start = Math.max(0, length - end);
30 end = length - 1;
31 } else if (end == -1 || end >= length) {
32 end = length - 1;
33 }
34 
35 if (start > end || start >= length) {
36 return ResponseEntity.status(HttpStatus.REQUESTABLE_RANGE_NOT_SATISFIABLE)
37 .header(HttpHeaders.CONTENT_RANGE, "bytes */" + length)
38 .build();
39 }
40 
41 long rangeStart = start;
42 long rangeLength = end - start + 1;
43 
44 StreamingResponseBody body = out -> {
45 try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
46 channel.position(rangeStart);
47 ByteBuffer buffer = ByteBuffer.allocate(64 * 1024);
48 long remaining = rangeLength;
49 while (remaining > 0 && channel.read(buffer) != -1) {
50 buffer.flip();
51 int toWrite = (int) Math.min(buffer.remaining(), remaining);
52 out.write(buffer.array(), buffer.position(), toWrite);
53 remaining -= toWrite;
54 buffer.clear();
55 }
56 }
57 };
58 
59 return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
60 .header(HttpHeaders.ACCEPT_RANGES, "bytes")
61 .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + length)
62 .contentType(MediaType.parseMediaType(contentType))
63 .contentLength(rangeLength)
64 .body(body);
65}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Advertising Accept-Ranges and honoring the Range header lets clients resume and seek downloads.
  2. 2StreamingResponseBody streams bytes lazily so large files never load fully into memory.
  3. 3A malformed or unsatisfiable range must answer 416 with a Content-Range that reports the full length.

Related explainers

Share this explainer

Here's the card — post it anywhere.

HTTP range requests in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code