Implementing HTTP Range Requests for Partial Content Download with Spring Boot
This article explains how to use HTTP Range headers for resumable downloads, how to detect server support, the relevant status codes, proper Range syntax, error handling, and provides a complete Spring Boot example that integrates with Ceph storage to serve partial file content.
When network speed is slow or unstable, downloading large objects can fail, so supporting resumable (range) downloads becomes essential.
The client sends a GET request with a Range header to specify the byte position from which the server should start sending data.
Support for range requests can be determined by checking the Accept-Ranges response header, as described in RFC 2616 Section 14.35.1 (Byte Ranges).
boolean support = urlConnection.getHeaderField("Accept-Ranges").equals("bytes");
System.out.println("Partial content retrieval support = " + (support ? "Yes" : "No"));Example using curl to request the first 10 bytes of a file:
curl -i --range 0-9 http://localhost:8080/file/chunk/download
HTTP/1.1 206
Accept-Ranges: bytes
Content-Disposition: inline;filename=pom.xml
Content-Range: bytes 0-9/13485
Content-Length: 10
Date: Mon, 01 Nov 2021 09:53:25 GMTA HEAD request (e.g., HeadObject ) can retrieve object metadata without returning the body.
HEAD /ObjectName HTTP/1.1
Host: BucketName.oss-cn-hangzhou.aliyuncs.com
Date: GMT Date
Authorization: SignatureValueRelevant HTTP status codes for range requests are:
206 Partial Content : the range request succeeded.
416 Requested Range Not Satisfiable : the requested range is outside the valid interval.
200 OK : the server does not support range requests and returns the full object.
Key points:
HTTP range requests require HTTP/1.1 or higher.
Use the Accept-Ranges header to detect support.
Specify the desired byte interval with the Range header.
The response includes Content-Range and Content-Length to describe the returned segment.
The If-Range header can be used with ETag or Last-Modified to verify resource changes.
Typical Range header values:
Range: bytes=0-499 – first 500 bytes.
Range: bytes=500-999 – next 500 bytes.
Range: bytes=-500 – last 500 bytes.
Range: bytes=500- – from byte 500 to the end.
Range: bytes=0- – the whole file.
If a range request is illegal, the server returns 200 OK with the full object; otherwise a valid request returns 206 with Content-Range .
Examples of illegal ranges for a 1000‑byte object (valid range 0‑999): Range: byte=0-499 – wrong unit (should be bytes ). Range: bytes=0-1000 – end byte exceeds size. Range: bytes=1000-2000 – start byte exceeds size. Range: bytes=1000- – start byte exceeds size. Range: bytes=-2000 – negative suffix larger than object.
Server‑side implementation using Spring Boot:
@Slf4j
@RestController
public class Controller {
@Autowired
private FileService fileService;
@GetMapping("/oceanfile/download")
public void downloadOceanfile(@RequestParam String fileId,
@RequestHeader(value = "Range") String range,
HttpServletResponse response) {
this.fileService.downloadFile(fileId, response, range);
}
} @Slf4j
@Service
public class FileService {
@Autowired
private CephUtils cephUtils;
public void downloadFile(String fileId, HttpServletResponse response, String range) {
FileInfo fileInfo = getFileInfo(fileId);
String bucketName = fileInfo.getBucketName();
String relativePath = fileInfo.getRelativePath();
RangeDTO rangeInfo = executeRangeInfo(range, fileInfo.getFileSize());
if (Objects.isNull(rangeInfo)) {
cephUtils.downloadFile(response, bucketName, relativePath);
return;
}
cephUtils.downloadFileWithRange(response, bucketName, relativePath, rangeInfo);
}
private RangeDTO executeRangeInfo(String range, Long fileSize) {
if (StringUtils.isEmpty(range) || !range.contains("bytes=") || !range.contains("-")) {
return null;
}
long startByte = 0;
long endByte = fileSize - 1;
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split("-");
if (ranges.length <= 0 || ranges.length > 2) {
return null;
}
try {
if (ranges.length == 1) {
if (range.startsWith("-")) {
endByte = Long.parseLong(ranges[0]);
} else if (range.endsWith("-")) {
startByte = Long.parseLong(ranges[0]);
}
} else {
startByte = Long.parseLong(ranges[0]);
endByte = Long.parseLong(ranges[1]);
}
} catch (NumberFormatException e) {
startByte = 0;
endByte = fileSize - 1;
}
if (startByte >= fileSize) {
log.error("range error, startByte >= fileSize. startByte: {}, fileSize: {}", startByte, fileSize);
return null;
}
return new RangeDTO(startByte, endByte);
}
}Save this guide for quick reference when you need to implement resumable downloads in Java back‑end services.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.