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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Advertising Accept-Ranges and honoring the Range header lets clients resume and seek downloads.
- 2StreamingResponseBody streams bytes lazily so large files never load fully into memory.
- 3A malformed or unsatisfiable range must answer 416 with a Content-Range that reports the full length.
Related explainers
java
public interface GitHubClient { @GetExchange("/users/{username}") GitHubUser getUser(@PathVariable String username);
Declarative HTTP clients in Spring
http-client
declarative-api
proxy
Intermediate
8 steps
java
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;
Building a trie for autocomplete in Java
trie
prefix-tree
recursion
Intermediate
8 steps
java
@RestController @RequestMapping("/api/products") public class ProductSearchController {
Binding collection query params in Spring
rest-api
query-parameters
dependency-injection
Intermediate
6 steps
java
import java.util.ArrayDeque; import java.util.Deque; import java.util.Map;
Evaluating math expressions with two stacks
stacks
parsing
operator-precedence
Intermediate
9 steps
go
func UploadChunk(c *gin.Context) { uploadID := c.Param("uploadID") if !validUploadID.MatchString(uploadID) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid upload id"})
Resumable chunked uploads in Gin
file-upload
content-range
streaming
Advanced
9 steps
rust
use axum::{ body::Body, extract::Path, http::{header, HeaderMap, HeaderValue, StatusCode},
HTTP range requests for video streaming in Axum
http-range-requests
streaming
async-io
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/http-range-requests-in-spring-explained-java-d163/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.