From 3cd3f4a90bad0b14437f70100c717f8ed1779bd2 Mon Sep 17 00:00:00 2001 From: Braidon Date: Thu, 18 Jun 2026 22:48:26 +1000 Subject: [PATCH 1/2] Return 416 for invalid range requests in Plug.Static --- lib/plug/static.ex | 16 ++++++++++++++-- test/plug/static_test.exs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/plug/static.ex b/lib/plug/static.ex index d3c79113..98576a5a 100644 --- a/lib/plug/static.ex +++ b/lib/plug/static.ex @@ -289,6 +289,7 @@ defmodule Plug.Static do {range_start, range_end} <- start_and_end(bytes, file_size) do send_range(conn, path, range_start, range_end, file_size, options) else + :unsatisfiable -> send_unsatisfiable_range(conn, file_size, options) _ -> send_entire_file(conn, path, options) end end @@ -306,15 +307,18 @@ defmodule Plug.Static do defp start_and_end(range, file_size) do case Integer.parse(range) do - {first, "-"} when first >= 0 -> + {first, "-"} when first >= 0 and first < file_size -> {first, file_size - 1} - {first, "-" <> rest} when first >= 0 -> + {first, "-" <> rest} when first >= 0 and first < file_size -> case Integer.parse(rest) do {last, ""} when last >= first -> {first, min(last, file_size - 1)} _ -> :error end + {first, "-" <> _} when first >= file_size -> + :unsatisfiable + _ -> :error end @@ -333,6 +337,14 @@ defmodule Plug.Static do |> halt() end + defp send_unsatisfiable_range(conn, file_size, options) do + conn + |> maybe_add_vary(options) + |> put_resp_header("content-range", "bytes */#{file_size}") + |> send_resp(416, "") + |> halt() + end + defp send_entire_file(conn, path, options) do conn |> maybe_add_vary(options) diff --git a/test/plug/static_test.exs b/test/plug/static_test.exs index b116c8c9..ab4626b7 100644 --- a/test/plug/static_test.exs +++ b/test/plug/static_test.exs @@ -468,6 +468,40 @@ defmodule Plug.StaticTest do assert get_resp_header(conn, "content-type") == ["text/plain"] end + test "returns 416 if range start is past the end of the file" do + conn = + conn(:get, "/public/fixtures/static.txt", []) + |> put_req_header("range", "bytes=10-20") + |> call() + + assert conn.status == 416 + assert conn.resp_body == "" + assert get_resp_header(conn, "content-range") == ["bytes */5"] + assert get_resp_header(conn, "accept-ranges") == ["bytes"] + end + + test "returns 416 if open-ended range start is past the end of the file" do + conn = + conn(:get, "/public/fixtures/static.txt", []) + |> put_req_header("range", "bytes=10-") + |> call() + + assert conn.status == 416 + assert get_resp_header(conn, "content-range") == ["bytes */5"] + end + + test "returns 416 if range start equals the file size" do + for range <- ["bytes=5-", "bytes=5-9"] do + conn = + conn(:get, "/public/fixtures/static.txt", []) + |> put_req_header("range", range) + |> call() + + assert conn.status == 416, "expected 416 for #{range}" + assert get_resp_header(conn, "content-range") == ["bytes */5"] + end + end + test "performs etag negotiation" do conn = conn(:get, "/public/fixtures/static.txt") From f870c5bc2a1f6460c919d77ef49cf4246df0a81a Mon Sep 17 00:00:00 2001 From: Braidon Date: Thu, 18 Jun 2026 22:48:47 +1000 Subject: [PATCH 2/2] Ignore range requests for 0-byte files in Plug.Static --- lib/plug/static.ex | 2 ++ test/fixtures/empty.txt | 0 test/plug/static_test.exs | 12 ++++++++++++ 3 files changed, 14 insertions(+) create mode 100644 test/fixtures/empty.txt diff --git a/lib/plug/static.ex b/lib/plug/static.ex index 98576a5a..4e1f87f8 100644 --- a/lib/plug/static.ex +++ b/lib/plug/static.ex @@ -298,6 +298,8 @@ defmodule Plug.Static do send_entire_file(conn, path, options) end + defp start_and_end(_range, 0), do: :error + defp start_and_end("-" <> rest, file_size) do case Integer.parse(rest) do {last, ""} when last > 0 and last <= file_size -> {file_size - last, file_size - 1} diff --git a/test/fixtures/empty.txt b/test/fixtures/empty.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/plug/static_test.exs b/test/plug/static_test.exs index ab4626b7..71286992 100644 --- a/test/plug/static_test.exs +++ b/test/plug/static_test.exs @@ -502,6 +502,18 @@ defmodule Plug.StaticTest do end end + test "ignores range and serves the file when it is zero bytes" do + for range <- ["bytes=0-", "bytes=1-", "bytes=10-20"] do + conn = + conn(:get, "/public/fixtures/empty.txt", []) + |> put_req_header("range", range) + |> call() + + assert conn.status == 200, "expected 200 for #{range}" + assert conn.resp_body == "" + end + end + test "performs etag negotiation" do conn = conn(:get, "/public/fixtures/static.txt")