Fix HTTP Range requests on dataset files (non-nginx deployments)

send_file read file_size from the content-length response header, but
files served from a dataset's extra_files_path (e.g. JBrowse track data)
don't set it, so any Range request 500'd with KeyError. nginx/apache
deployments dodged this via the X-Accel-Redirect/X-Sendfile early return.

Use os.path.getsize(body.name) instead, and honor the requested range
end (the old 'end is None' check was always true, so it ignored the
client's end and always served to EOF).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John Chilton
2026-06-15 15:43:05 -04:00
co-authored by Claude Opus 4.8
parent 19c75c6852
commit 9e4be516e7
2 changed files with 36 additions and 4 deletions
+2 -2
View File
@@ -569,8 +569,8 @@ def send_file(start_response, trans, body):
body = b""
if trans.request.range:
start = int(trans.request.range.start)
file_size = int(trans.response.headers["content-length"])
end = int(file_size if end is None else trans.request.range.end)
file_size = os.path.getsize(body.name)
end = file_size if trans.request.range.end is None else min(int(trans.request.range.end), file_size)
trans.response.headers["content-length"] = str(end - start)
trans.response.headers["content-range"] = f"bytes {start}-{end - 1}/{file_size}"
trans.response.status = 206
+34 -2
View File
@@ -15,7 +15,7 @@ from galaxy.web.framework.base import (
CONTENT = "content"
def setup_fastAPI(fh, nginx_x_accel_redirect_base=None, apache_xsendfile=None):
def setup_fastAPI(fh, nginx_x_accel_redirect_base=None, apache_xsendfile=None, set_content_length=True):
def wsgi_application(env, start_response):
trans = Bunch(
response=Response(),
@@ -24,7 +24,8 @@ def setup_fastAPI(fh, nginx_x_accel_redirect_base=None, apache_xsendfile=None):
config=Bunch(nginx_x_accel_redirect_base=nginx_x_accel_redirect_base, apache_xsendfile=apache_xsendfile)
),
)
trans.response.headers["content-length"] = len(CONTENT)
if set_content_length:
trans.response.headers["content-length"] = len(CONTENT)
trans.response.set_content_type("application/octet-stream")
return send_file(start_response, trans, fh)
@@ -72,3 +73,34 @@ def test_sendfile_in_process(test_file_handle):
assert "x-accel-redirect" not in response.headers
assert response.headers["content-length"] == str(len(CONTENT))
assert response.content.decode() == "content"
def test_sendfile_range(test_file_handle):
app = setup_fastAPI(test_file_handle)
client = TestClient(app)
response = client.get("/test/send_file", headers={"Range": "bytes=0-3"})
assert response.status_code == 206
assert response.headers["content-range"] == f"bytes 0-3/{len(CONTENT)}"
assert response.headers["content-length"] == "4"
assert response.content.decode() == "cont"
def test_sendfile_range_open_ended(test_file_handle):
app = setup_fastAPI(test_file_handle)
client = TestClient(app)
response = client.get("/test/send_file", headers={"Range": "bytes=2-"})
assert response.status_code == 206
assert response.headers["content-range"] == f"bytes 2-{len(CONTENT) - 1}/{len(CONTENT)}"
assert response.headers["content-length"] == str(len(CONTENT) - 2)
assert response.content.decode() == "ntent"
def test_sendfile_range_without_content_length(test_file_handle):
# Files served out of a dataset's extra_files_path (e.g. JBrowse track data) don't set
# a content-length header; a Range request on a non-nginx deployment must still succeed.
app = setup_fastAPI(test_file_handle, set_content_length=False)
client = TestClient(app)
response = client.get("/test/send_file", headers={"Range": "bytes=0-3"})
assert response.status_code == 206
assert response.headers["content-range"] == f"bytes 0-3/{len(CONTENT)}"
assert response.content.decode() == "cont"