Coverage for src/local_deep_research/web/utils/range_response.py: 100%
42 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""Byte-range responses for content already loaded into memory."""
3from collections.abc import Mapping
4import re
6from starlette.requests import Request
7from starlette.responses import Response
10def in_memory_range_response(
11 content: bytes,
12 *,
13 request: Request,
14 media_type: str,
15 headers: Mapping[str, str] | None = None,
16) -> Response:
17 """Honor one GET byte range without writing the content to a file.
19 Unsupported or malformed ranges use the complete response. These downloads
20 have no ETag or Last-Modified validator, so If-Range also selects the full
21 response instead of risking a slice from a changed document.
22 """
23 response = Response(content, media_type=media_type, headers=headers)
24 response.headers["Accept-Ranges"] = "bytes"
25 if request.method != "GET" or "if-range" in request.headers:
26 return response
28 ranges = request.headers.getlist("range")
29 # Do not partially honor a multi-range request or generate multipart data.
30 # Ignore oversized numeric input before the parser converts it to integers.
31 if len(ranges) != 1 or len(ranges[0]) > 1024 or "," in ranges[0]:
32 return response
33 unit, separator, value = ranges[0].partition("=")
34 if not separator or unit.strip(" \t").lower() != "bytes":
35 return response
36 match = re.fullmatch(r"([0-9]*)[ \t]*-[ \t]*([0-9]*)", value.strip(" \t"))
37 if match is None or not any(match.groups()):
38 return response
40 length = len(content)
41 first, last = match.groups()
42 try:
43 if not first:
44 suffix_length = int(last)
45 if suffix_length == 0:
46 return response
47 start, stop = max(0, length - suffix_length), length
48 else:
49 start = int(first)
50 stop = int(last) + 1 if last else length
51 if last and stop <= start:
52 return response
53 except ValueError:
54 # Respect an interpreter's configured integer-conversion limit too.
55 return response
57 if start >= length:
58 response.status_code = 416
59 response.body = b""
60 # nosemgrep: semgrep.rules.sql-string-concatenation, reason: HTTP Content-Range uses integer byte offsets and never reaches SQL
61 response.headers["Content-Range"] = f"bytes */{length}"
62 else:
63 stop = min(stop, length)
64 response.status_code = 206
65 response.body = content[start:stop]
66 # nosemgrep: semgrep.rules.sql-string-concatenation, reason: HTTP Content-Range uses integer byte offsets and never reaches SQL
67 response.headers["Content-Range"] = f"bytes {start}-{stop - 1}/{length}"
68 response.headers["Content-Length"] = str(len(response.body))
69 return response