Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docling/backend/html_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from docling.datamodel.base_models import InputFormat
from docling.datamodel.document import InputDocument
from docling.exceptions import OperationNotAllowed
from docling.utils.http_client import request_with_retry

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -1256,9 +1257,9 @@ def _load_image_data(self, src_loc: str) -> Optional[bytes]:
"Fetching remote resources is only allowed when set explicitly. "
"Set options.enable_remote_fetch=True."
)
response = requests.get(src_loc, stream=True)
response.raise_for_status()
return response.content
with request_with_retry("GET", src_loc, stream=True) as response:
response.raise_for_status()
return response.content
elif src_loc.startswith("data:"):
data = re.sub(r"^data:image/.+;base64,", "", src_loc)
return base64.b64decode(data)
Expand Down
16 changes: 11 additions & 5 deletions docling/utils/api_image_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import json
import logging
from io import BytesIO
from typing import Dict, List, Optional, Tuple
from typing import Optional, Tuple

import requests
from PIL import Image
from pydantic import AnyUrl

from docling.datamodel.base_models import OpenAiApiResponse, VlmStopReason
from docling.models.utils.generation_utils import GenerationStopper
from docling.utils.http_client import request_with_retry

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -63,7 +63,8 @@ def api_image_request(

headers = headers or {}

r = requests.post(
r = request_with_retry(
"POST",
str(url),
headers=headers,
json=payload,
Expand Down Expand Up @@ -142,8 +143,13 @@ def api_image_request_streaming(
hdrs["X-Temperature"] = str(params["temperature"])

# Stream the HTTP response
with requests.post(
str(url), headers=hdrs, json=payload, timeout=timeout, stream=True
with request_with_retry(
"POST",
str(url),
headers=hdrs,
json=payload,
timeout=timeout,
stream=True,
) as r:
if not r.ok:
_log.error(
Expand Down
79 changes: 79 additions & 0 deletions docling/utils/http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from typing import Collection, Iterable

import requests
from requests import Response, Session
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# Status codes worth retrying because they are transient or throttling related.
_DEFAULT_STATUS_FORCELIST = (408, 425, 429, 500, 502, 503, 504)

# Methods that are safe or idempotent enough for retries in our usage.
_DEFAULT_ALLOWED_METHODS = frozenset(
["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
)


def _build_retry(
*,
total: int = 5,
backoff_factor: float = 0.2,
status_forcelist: Collection[int] = _DEFAULT_STATUS_FORCELIST,
allowed_methods: Iterable[str] | None = _DEFAULT_ALLOWED_METHODS,
) -> Retry:
return Retry(
total=total,
read=total,
connect=total,
status=total,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
allowed_methods=frozenset(allowed_methods) if allowed_methods else None,
raise_on_status=False,
)


def create_retry_session(
*,
total: int = 5,
backoff_factor: float = 0.2,
status_forcelist: Collection[int] = _DEFAULT_STATUS_FORCELIST,
allowed_methods: Iterable[str] | None = _DEFAULT_ALLOWED_METHODS,
) -> Session:
"""Return a requests Session configured with retry/backoff handling."""
session = requests.Session()
retry = _build_retry(
total=total,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
allowed_methods=allowed_methods,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session


_DEFAULT_SESSION: Session | None = None


def get_retry_session() -> Session:
"""Return the lazily-created default retry-enabled Session."""
global _DEFAULT_SESSION
if _DEFAULT_SESSION is None:
_DEFAULT_SESSION = create_retry_session()
return _DEFAULT_SESSION


def request_with_retry(
method: str,
url: str,
*,
session: Session | None = None,
**kwargs,
) -> Response:
"""Perform an HTTP request using a retry-enabled Session."""
sess = session or get_retry_session()
return sess.request(method=method, url=url, **kwargs)
5 changes: 3 additions & 2 deletions docling/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from pathlib import Path
from typing import List, Union

import requests
from tqdm import tqdm

from docling.utils.http_client import request_with_retry


def chunkify(iterator, chunk_size):
"""Yield successive chunks of chunk_size from the iterable."""
Expand Down Expand Up @@ -46,7 +47,7 @@ def create_hash(string: str):

def download_url_with_progress(url: str, progress: bool = False) -> BytesIO:
buf = BytesIO()
with requests.get(url, stream=True, allow_redirects=True) as response:
with request_with_retry("GET", url, stream=True, allow_redirects=True) as response:
total_size = int(response.headers.get("content-length", 0))
progress_bar = tqdm(
total=total_size,
Expand Down
Loading