From bd3efacfafe5428e4539f4607eceed4aad7e3bd3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 20:28:29 +1200 Subject: [PATCH] Return 416 for unsatisfiable byte ranges. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- lib/utopia/static/local_file.rb | 10 +++++++--- releases.md | 1 + test/utopia/static.rb | 24 +++++++++++++++++++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/utopia/static/local_file.rb b/lib/utopia/static/local_file.rb index e9ddf41e..556b2847 100644 --- a/lib/utopia/static/local_file.rb +++ b/lib/utopia/static/local_file.rb @@ -69,9 +69,13 @@ def serve(request, response_headers) ranges = byte_ranges(request) size = bytesize - # puts "Requesting ranges: #{ranges.inspect} (#{size})" - - if ranges == nil or ranges.size != 1 + # A valid byte-range request with no satisfiable ranges cannot be fulfilled: + if ranges && ranges.empty? + response_headers[CONTENT_LENGTH] = "0" + response_headers[CONTENT_RANGE] = "bytes */#{size}" + + return Response[416, response_headers, []] + elsif ranges == nil or ranges.size != 1 # No ranges, or multiple ranges (which we don't support). # TODO: Support multiple byte-ranges, for now just send entire file: status = 200 diff --git a/releases.md b/releases.md index 94ac571d..d396a147 100644 --- a/releases.md +++ b/releases.md @@ -5,6 +5,7 @@ - **Breaking** Remove support for JavaScript packages installed in `lib/components`; use `node_modules` instead. - **Security** Authenticate encrypted session cookies using AES-256-GCM. Existing session cookies are invalidated. - **Security** Redact sensitive exception report fields and make bounded request body attachments opt-in. + - Return `416 Range Not Satisfiable` for unsatisfiable static file byte ranges. ## v3.0.0 diff --git a/test/utopia/static.rb b/test/utopia/static.rb index bd567de2..09974da6 100755 --- a/test/utopia/static.rb +++ b/test/utopia/static.rb @@ -223,11 +223,29 @@ expect(last_response.read).to be == "Hello World!" end - it "should ignore unsatisfiable ranges" do + it "returns range not satisfiable when no ranges can be fulfilled" do client.get "/test.txt", {"range" => "bytes=999-1000"} - expect(last_response.status).to be == 200 - expect(last_response.read).to be == "Hello World!" + expect(last_response.status).to be == 416 + expect(last_response.headers["content-length"]).to be == "0" + expect(last_response.headers["content-range"]).to be == "bytes */12" + expect(last_response.read).to be_nil + end + + it "returns range not satisfiable when several ranges cannot be fulfilled" do + client.get "/test.txt", {"range" => "bytes=999-1000,2000-3000"} + + expect(last_response.status).to be == 416 + expect(last_response.headers["content-range"]).to be == "bytes */12" + expect(last_response.read).to be_nil + end + + it "returns a single satisfiable range from a mixed range set" do + client.get "/test.txt", {"range" => "bytes=0-1,999-1000"} + + expect(last_response.status).to be == 206 + expect(last_response.headers["content-range"]).to be == "bytes 0-1/12" + expect(last_response.read).to be == "He" end it "should ignore multiple ranges" do