{"record":{"id":"eee54151f360d4ae","repo":"commaai/openpilot","slug":"expected-len-ranges-parts-got-len-parts-s","errorCode":null,"errorMessage":"Expected {len(ranges)} parts, got {len(parts)} ({self._url})","messagePattern":"Expected (.+?) parts, got (.+?) \\((.+?)\\)","errorType":"exception","errorClass":"URLFileException","httpStatus":null,"severity":"error","filePath":"openpilot/tools/lib/url_file.py","lineNumber":192,"sourceCode":"\n    ctype = (r.headers.get(\"content-type\") or \"\").lower()\n    if \"multipart/byteranges\" not in ctype:\n      return [r.data,]\n\n    m = re.search(r'boundary=\"?([^\";]+)\"?', ctype)\n    if not m:\n      raise URLFileException(f\"Missing multipart boundary ({self._url})\")\n    boundary = m.group(1).encode()\n\n    parts = []\n    for chunk in r.data.split(b\"--\" + boundary):\n      if b\"\\r\\n\\r\\n\" not in chunk:\n        continue\n      payload = chunk.split(b\"\\r\\n\\r\\n\", 1)[1].rstrip(b\"\\r\\n\")\n      if payload and payload != b\"--\":\n        parts.append(payload)\n    if len(parts) != len(ranges):\n      raise URLFileException(f\"Expected {len(ranges)} parts, got {len(parts)} ({self._url})\")\n    return parts\n\n  def seekable(self) -> bool:\n    return True\n\n  def seek(self, pos: int, whence: int = 0) -> int:\n    pos = int(pos)\n    if whence == os.SEEK_SET:\n      self._pos = pos\n    elif whence == os.SEEK_CUR:\n      self._pos += pos\n    elif whence == os.SEEK_END:\n      length = self.get_length()\n      assert length != -1, \"Cannot seek from end on unknown length file\"\n      self._pos = length + pos\n    else:\n      raise URLFileException(\"Invalid whence value\")\n    return self._pos","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/commaai/openpilot/blob/516ec1e68203439a73f340f1d0b3b91eabc626ee/openpilot/tools/lib/url_file.py#L174-L210","documentation":"After splitting a multipart/byteranges response by boundary, url_file counts the payload parts; if the count differs from the number of requested ranges it raises. Causes: server coalesced/dropped ranges, malformed boundaries causing over-splitting, or a 200 full-file response on a multipart content-type.","triggerScenarios":"get_multi_range() with N>1 ranges where the server returns fewer/more parts than requested, or boundary bytes appearing inside payload data causing spurious splits.","commonSituations":"Non-compliant proxies/CDNs rewriting ranged responses; overlapping or zero-width ranges after the e>s filter; servers that answer multi-range requests with only the first range.","solutions":["Fall back to one range per request (list of single-range calls) — most robust across servers","Verify each requested range satisfies s<e and ranges don't overlap","Compare raw response bytes with curl -H 'Range: bytes=0-1,5-6' to see what the server actually returns"],"exampleFix":"# before\nparts = uf.get_multi_range(ranges)\n\n# after\nfrom openpilot.tools.lib.url_file import URLFileException\ntry:\n    parts = uf.get_multi_range(ranges)\nexcept URLFileException:\n    parts = [uf.get_multi_range([r])[0] for r in ranges]","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    parts = uf.get_multi_range(ranges)\nexcept URLFileException as e:\n    if 'parts, got' in str(e):\n        parts = [uf.get_multi_range([r])[0] for r in ranges]","preventionTips":["Ensure ranges are sorted, non-overlapping, and satisfy start < end","Prefer single-range requests unless the server is known-compliant","Validate part counts defensively when parsing multipart yourself"],"tags":["network","http","multipart","url-file"],"backgroundTag":null,"analyzedSha":"516ec1e68203439a73f340f1d0b3b91eabc626ee","analyzedAt":"2026-08-15T00:17:37.461Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}