{"record":{"id":"f71be45e28e62dbd","repo":"docling-project/docling","slug":"artifact-download-redirect-is-missing-a-location-h","errorCode":null,"errorMessage":"Artifact download redirect is missing a Location header.","messagePattern":"Artifact download redirect is missing a Location header\\.","errorType":"exception","errorClass":"ArtifactDownloadError","httpStatus":null,"severity":"error","filePath":"docling/service_client/client.py","lineNumber":1959,"sourceCode":"                raise ArtifactDownloadError(\n                    \"Too many redirects while downloading artifact.\"\n                )\n        except httpx.HTTPError as exc:\n            raise ArtifactDownloadError(f\"Artifact download failed: {exc}\") from exc\n\n    def _validate_artifact_url(self, url: str) -> None:\n        if self._allow_private_artifact_urls:\n            return\n        if not _is_safe_artifact_url(url):\n            raise ArtifactDownloadError(\n                f\"Refusing to download artifact from a non-public URL: {url}.\"\n            )\n\n    @staticmethod\n    def _next_redirect_url(current_url: str, response: httpx.Response) -> str:\n        location = response.headers.get(\"location\")\n        if not location:\n            raise ArtifactDownloadError(\n                \"Artifact download redirect is missing a Location header.\"\n            )\n        return str(httpx.URL(current_url).join(location))\n\n    def _check_artifact_size(self, total: int) -> None:\n        if total > self._max_artifact_download_bytes:\n            raise ArtifactDownloadError(\n                \"Artifact exceeds max_artifact_download_bytes \"\n                f\"({self._max_artifact_download_bytes} bytes).\"\n            )\n\n    def _source_to_upload_files(\n        self,\n        source: Path | DocumentStream,\n    ) -> dict[str, tuple[str, IO[bytes], str]]:\n        \"\"\"Build multipart files dict for a sync upload. Passes file handles — no full read.\"\"\"\n        if isinstance(source, Path):\n            filename = source.name","sourceCodeStart":1941,"sourceCodeEnd":1977,"githubUrl":"https://github.com/docling-project/docling/blob/61d76f1ff3f8428065465889f7b4577da7df704c/docling/service_client/client.py#L1941-L1977","documentation":"Raised as ArtifactDownloadError by _next_redirect_url when a response is classified as a redirect (3xx) but carries no Location header. The manual redirect follower cannot determine the next URL, so the download aborts.","triggerScenarios":"Artifact server returns 301/302/307/308 with a missing or empty Location header — typically a misconfigured reverse proxy or a stripped header.","commonSituations":"Proxies that drop Location on redirect; object-store gateways with buggy redirect responses; security appliances rewriting responses.","solutions":["Reproduce with curl -i against the artifact URL and inspect the 3xx response headers","Fix the proxy/gateway to forward or set the Location header","Bypass the offending proxy layer for artifact traffic"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"import requests\nr = requests.get(artifact_url, allow_redirects=False)\nif r.is_redirect:\n    assert r.headers.get('location'), 'proxy strips Location header'","typeGuard":"def is_artifact_download_error(exc: BaseException) -> bool:\n    return isinstance(exc, ArtifactDownloadError)","tryCatchPattern":"try:\n    results = list(client.convert_all(sources))\nexcept ArtifactDownloadError as exc:\n    if 'missing a Location header' in str(exc):\n        alert_ops('artifact proxy strips Location on redirects')","preventionTips":["Validate redirect headers when introducing new proxies in front of object storage","Keep proxies RFC-compliant for 3xx responses"],"tags":["network","redirects","http-headers","artifact-download"],"backgroundTag":null,"analyzedSha":"61d76f1ff3f8428065465889f7b4577da7df704c","analyzedAt":"2026-08-14T23:53:18.727Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}