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
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
Intermediate
8 steps
java
public final class EncodingDetector { public enum Encoding { UTF_8, UTF_16LE, UTF_16BE, UTF_32LE, UTF_32BE, ASCII, UNKNOWN
Detecting text encoding from raw bytes in Java
byte-manipulation
encoding-detection
bitwise-operations
Intermediate
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.