rust
61 lines · 8 steps
HTTP range requests for video streaming in Axum
An Axum handler serves partial video content so browsers can seek and stream without downloading the whole file.
Explained by
highlit
1use axum::{
2 body::Body,
3 extract::Path,
4 http::{header, HeaderMap, HeaderValue, StatusCode},
5 response::{IntoResponse, Response},
6};
7use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
8use tokio_util::io::ReaderStream;
9
10pub async fn stream_video(Path(name): Path<String>, headers: HeaderMap) -> Response {
11 let path = std::path::Path::new("media").join(&name);
12 let mut file = match tokio::fs::File::open(&path).await {
13 Ok(f) => f,
14 Err(_) => return StatusCode::NOT_FOUND.into_response(),
15 };
16 let total = match file.metadata().await {
17 Ok(m) => m.len(),
18 Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
19 };
20
21 let range = headers.get(header::RANGE).and_then(|v| v.to_str().ok());
22 let (start, end) = match parse_range(range, total) {
23 Some(r) => r,
24 None if range.is_some() => {
25 return Response::builder()
26 .status(StatusCode::RANGE_NOT_SATISFIABLE)
27 .header(header::CONTENT_RANGE, format!("bytes */{total}"))
28 .body(Body::empty())
29 .unwrap();
30 }
31 None => (0, total - 1),
32 };
33
34 let len = end - start + 1;
35 if file.seek(SeekFrom::Start(start)).await.is_err() {
36 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
37 }
38 let stream = ReaderStream::new(file.take(len));
39
40 let status = if range.is_some() { StatusCode::PARTIAL_CONTENT } else { StatusCode::OK };
41 let mut builder = Response::builder()
42 .status(status)
43 .header(header::CONTENT_TYPE, "video/mp4")
44 .header(header::ACCEPT_RANGES, "bytes")
45 .header(header::CONTENT_LENGTH, len);
46 if range.is_some() {
47 builder = builder.header(
48 header::CONTENT_RANGE,
49 HeaderValue::from_str(&format!("bytes {start}-{end}/{total}")).unwrap(),
50 );
51 }
52 builder.body(Body::from_stream(stream)).unwrap()
53}
54
55fn parse_range(header: Option<&str>, total: u64) -> Option<(u64, u64)> {
56 let spec = header?.strip_prefix("bytes=")?;
57 let (s, e) = spec.split_once('-')?;
58 let start: u64 = if s.is_empty() { 0 } else { s.parse().ok()? };
59 let end: u64 = if e.is_empty() { total - 1 } else { e.parse().ok()?.min(total - 1) };
60 (start <= end && start < total).then_some((start, end))
61}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Range requests let clients ask for byte slices, which is what makes video seeking work over HTTP.
- 2Streaming a bounded reader avoids loading an entire file into memory before responding.
- 3A malformed range must answer 416 with a Content-Range header rather than silently serving the whole file.
Related explainers
rust
use axum::{extract::{Path, State}, http::StatusCode, Json}; use dashmap::DashMap; use serde::Serialize; use std::sync::Arc;
Request coalescing in an Axum handler
caching
concurrency
request-coalescing
Advanced
8 steps
rust
use std::net::Ipv4Addr; use std::str::FromStr; #[derive(Debug, Clone, Copy)]
Parsing and matching IPv4 CIDR ranges in Rust
bitwise-operations
parsing
error-handling
Intermediate
8 steps
rust
use std::sync::OnceLock; static CRC_TABLE: OnceLock<[u32; 256]> = OnceLock::new();
Table-driven CRC32 with lazy init in Rust
checksums
lazy-initialization
lookup-tables
Intermediate
8 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 std::sync::mpsc; use std::thread; use std::time::Duration;
Running work with a timeout in Rust
concurrency
channels
timeout
Intermediate
7 steps
java
@GetMapping("/files/{id}") public ResponseEntity<StreamingResponseBody> download( @PathVariable String id, @RequestHeader(value = HttpHeaders.RANGE, required = false) String rangeHeader) throws IOException {
HTTP range requests in Spring
http-range
streaming
file-io
Advanced
10 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-for-video-streaming-in-axum-explained-rust-baf7/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.