From f37820b0947a9e5285110d46959421f5fc6ebde2 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Sun, 11 Oct 2026 10:48:17 +0300 Subject: [PATCH] Return nil from content_range for an unsatisfied range A 416 Range Not Satisfiable response may carry `Content-Range: bytes */1000` (RFC 9110 Section 14.4). `content_range` only accepted the `first-last/length` form, so it raised `Net::HTTPHeaderSyntaxError` for this valid value. `range_length` did too, and `read_body` calls it when there is no Content-Length, so reading the body of such a response failed. Accept the unsatisfied-range form and return nil, since it has no byte range. The body is then read to EOF as before. --- lib/net/http/header.rb | 7 ++++++- test/net/http/test_httpheader.rb | 6 ++++++ test/net/http/test_httpresponse.rb | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb index d6f12956..547e7cd3 100644 --- a/lib/net/http/header.rb +++ b/lib/net/http/header.rb @@ -712,11 +712,16 @@ def chunked? # res['Content-Range'] # => "bytes 0-499/1000" # res.content_range # => 0..499 # + # Returns +nil+ for an unsatisfied range such as 'bytes */1000', + # which a 416 Range Not Satisfiable response may carry; + # see {RFC 9110 Section 14.4}[https://www.rfc-editor.org/rfc/rfc9110.html#section-14.4]. + # def content_range return nil unless @header['content-range'] - m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or + m = %r<\A\s*(\w+)\s+(?:(\d+)-(\d+)/(?:\d+|\*)|\*/\d+)>.match(self['Content-Range']) or raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' return unless m[1] == 'bytes' + return unless m[2] m[2].to_i .. m[3].to_i end diff --git a/test/net/http/test_httpheader.rb b/test/net/http/test_httpheader.rb index b86d4868..004dd5d7 100644 --- a/test/net/http/test_httpheader.rb +++ b/test/net/http/test_httpheader.rb @@ -351,10 +351,14 @@ def test_content_range assert_equal 1..1, @c.content_range @c['Content-Range'] = "tokens 1-1/1000" assert_equal nil, @c.content_range + @c['Content-Range'] = "bytes */1000" + assert_equal nil, @c.content_range try_invalid_content_range "invalid" try_invalid_content_range "bytes 123-abc" try_invalid_content_range "bytes abc-123" + try_invalid_content_range "bytes */abc" + try_invalid_content_range "bytes */*" end def test_range_length @@ -366,6 +370,8 @@ def test_range_length assert_equal 1, @c.range_length @c['Content-Range'] = "tokens 1-1/1000" assert_equal nil, @c.range_length + @c['Content-Range'] = "bytes */1000" + assert_equal nil, @c.range_length try_invalid_content_range "bytes 1-1/abc" end diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb index e902b4f3..55e5be6e 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -730,6 +730,27 @@ def test_read_body_content_encoding_deflate_content_range assert_equal "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03", body end + def test_read_body_unsatisfied_content_range + io = dummy_io(<