{"record":{"id":"ad8cb371f6dec787","repo":"docling-project/docling","slug":"refusing-to-download-artifact-from-a-non-public-ur","errorCode":null,"errorMessage":"Refusing to download artifact from a non-public URL: {url}.","messagePattern":"Refusing to download artifact from a non-public URL: (.+?)\\.","errorType":"exception","errorClass":"ArtifactDownloadError","httpStatus":null,"severity":"error","filePath":"docling/service_client/client.py","lineNumber":1951,"sourceCode":"                            )\n                        chunks: list[bytes] = []\n                        total = 0\n                        async for chunk in response.aiter_bytes():\n                            total += len(chunk)\n                            self._check_artifact_size(total)\n                            chunks.append(chunk)\n                        return b\"\".join(chunks)\n                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            )","sourceCodeStart":1933,"sourceCodeEnd":1969,"githubUrl":"https://github.com/docling-project/docling/blob/61d76f1ff3f8428065465889f7b4577da7df704c/docling/service_client/client.py#L1933-L1969","documentation":"SSRF guard: _validate_artifact_url raises ArtifactDownloadError when the artifact URL (initial or any redirect hop) does not resolve to a globally routable address — non-http(s) scheme, missing host, DNS failure, or a private/loopback/link-local/reserved/multicast IP. The check is skipped only when the client was constructed with _allow_private_artifact_urls=True.","triggerScenarios":"docling-serve returns presigned URLs pointing at an internal hostname (e.g. minio.internal, 10.x.x.x, 169.254.169.254) while the client runs elsewhere; or a redirect hop lands on a private address.","commonSituations":"Self-hosted docling-serve in Docker/Kubernetes where the object store (MinIO) is only reachable inside the cluster; DNS resolving to a private IP from the client's network; a compromised service attempting SSRF via redirects.","solutions":["If the private endpoint is trusted, construct the client with _allow_private_artifact_urls=True (opt-in escape hatch)","Better: expose the object store on a publicly routable address or run the client inside the same network with the flag set deliberately","Check that the redirect chain does not detour through an internal load balancer"],"exampleFix":"// before\nclient = DoclingServiceClient('https://docling.internal')\n# ArtifactDownloadError: Refusing to download artifact from a non-public URL\n\n// after (trusted internal MinIO)\nclient = DoclingServiceClient('https://docling.internal', _allow_private_artifact_urls=True)","handlingStrategy":"validation","validationCode":"from docling.service_client.client import _is_safe_artifact_url\n# before opting out, check what the client will see:\nassert _is_safe_artifact_url(presigned_url), 'URL resolves private — needs opt-in'","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 'non-public URL' in str(exc):\n        client = DoclingServiceClient(url, _allow_private_artifact_urls=True)  # deliberate trust decision","preventionTips":["Only set _allow_private_artifact_urls=True when the service and network are fully trusted","Prefer making the object store routable over disabling the SSRF guard"],"tags":["security","ssrf","network","self-hosted","artifact-download"],"backgroundTag":null,"analyzedSha":"61d76f1ff3f8428065465889f7b4577da7df704c","analyzedAt":"2026-08-14T23:53:18.727Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}