From 9e4be516e7708992f1d850bf7cf5f9f6b0efa250 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 15 Jun 2026 15:39:46 -0400 Subject: [PATCH] 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) --- lib/galaxy/web/framework/base.py | 4 ++-- test/unit/webapps/test_send_file.py | 36 +++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/web/framework/base.py b/lib/galaxy/web/framework/base.py index d97bf9bf85e..454aee79cc2 100644 --- a/lib/galaxy/web/framework/base.py +++ b/lib/galaxy/web/framework/base.py @@ -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 diff --git a/test/unit/webapps/test_send_file.py b/test/unit/webapps/test_send_file.py index 1130e6c8804..7a38315802a 100644 --- a/test/unit/webapps/test_send_file.py +++ b/test/unit/webapps/test_send_file.py @@ -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"