Claude commited on
Commit
1b0c69c
·
unverified ·
1 Parent(s): 021c7a8

fix(ingest): add User-Agent and headers to httpx requests to fix Gallica 403

Browse files

Gallica (BnF) rejects automated requests without a valid User-Agent.
Added _HEADERS to iiif_fetcher.py (image download) and _MANIFEST_HEADERS to
ingest.py (_fetch_json_manifest) with:
- User-Agent: Mozilla/5.0 (compatible; ScriptoriumAI/1.0; ...)
- Accept: appropriate MIME types for each request type
- Referer: https://gallica.bnf.fr/

Updated test_image_pipeline.py to assert the headers are passed in the mock call.

https://claude.ai/code/session_018woyEHc8HG2th7V4ewJ4Kg

backend/app/api/v1/ingest.py CHANGED
@@ -112,10 +112,20 @@ async def _create_page(
112
  return page
113
 
114
 
 
 
 
 
 
 
 
 
 
 
115
  async def _fetch_json_manifest(url: str) -> dict:
116
  """Télécharge un manifest IIIF. Fonction isolée pour faciliter les tests."""
117
  async with httpx.AsyncClient() as client:
118
- resp = await client.get(url, follow_redirects=True, timeout=30.0)
119
  resp.raise_for_status()
120
  return resp.json()
121
 
 
112
  return page
113
 
114
 
115
+ _MANIFEST_HEADERS = {
116
+ "User-Agent": (
117
+ "Mozilla/5.0 (compatible; ScriptoriumAI/1.0; "
118
+ "+https://huggingface.co/spaces/Ma-Ri-Ba-Ku/scriptorium-ai)"
119
+ ),
120
+ "Accept": "application/ld+json,application/json,*/*",
121
+ "Referer": "https://gallica.bnf.fr/",
122
+ }
123
+
124
+
125
  async def _fetch_json_manifest(url: str) -> dict:
126
  """Télécharge un manifest IIIF. Fonction isolée pour faciliter les tests."""
127
  async with httpx.AsyncClient() as client:
128
+ resp = await client.get(url, headers=_MANIFEST_HEADERS, follow_redirects=True, timeout=30.0)
129
  resp.raise_for_status()
130
  return resp.json()
131
 
backend/app/services/ingest/iiif_fetcher.py CHANGED
@@ -11,6 +11,15 @@ logger = logging.getLogger(__name__)
11
 
12
  _DEFAULT_TIMEOUT = 60.0 # secondes — les images IIIF haute résolution peuvent être lourdes
13
 
 
 
 
 
 
 
 
 
 
14
 
15
  def fetch_iiif_image(url: str, timeout: float = _DEFAULT_TIMEOUT) -> bytes:
16
  """Télécharge une image depuis une URL IIIF complète.
@@ -28,7 +37,7 @@ def fetch_iiif_image(url: str, timeout: float = _DEFAULT_TIMEOUT) -> bytes:
28
  httpx.RequestError: pour toute autre erreur réseau.
29
  """
30
  logger.info("Fetching IIIF image", extra={"url": url})
31
- response = httpx.get(url, follow_redirects=True, timeout=timeout)
32
  response.raise_for_status()
33
  logger.info(
34
  "IIIF image fetched",
 
11
 
12
  _DEFAULT_TIMEOUT = 60.0 # secondes — les images IIIF haute résolution peuvent être lourdes
13
 
14
+ _HEADERS = {
15
+ "User-Agent": (
16
+ "Mozilla/5.0 (compatible; ScriptoriumAI/1.0; "
17
+ "+https://huggingface.co/spaces/Ma-Ri-Ba-Ku/scriptorium-ai)"
18
+ ),
19
+ "Accept": "image/jpeg,image/png,image/*,*/*",
20
+ "Referer": "https://gallica.bnf.fr/",
21
+ }
22
+
23
 
24
  def fetch_iiif_image(url: str, timeout: float = _DEFAULT_TIMEOUT) -> bytes:
25
  """Télécharge une image depuis une URL IIIF complète.
 
37
  httpx.RequestError: pour toute autre erreur réseau.
38
  """
39
  logger.info("Fetching IIIF image", extra={"url": url})
40
+ response = httpx.get(url, headers=_HEADERS, follow_redirects=True, timeout=timeout)
41
  response.raise_for_status()
42
  logger.info(
43
  "IIIF image fetched",
backend/tests/test_image_pipeline.py CHANGED
@@ -272,6 +272,14 @@ def test_fetch_iiif_image_success():
272
  assert result == fake_bytes
273
  mock_get.assert_called_once_with(
274
  "https://example.com/image.jpg",
 
 
 
 
 
 
 
 
275
  follow_redirects=True,
276
  timeout=60.0,
277
  )
 
272
  assert result == fake_bytes
273
  mock_get.assert_called_once_with(
274
  "https://example.com/image.jpg",
275
+ headers={
276
+ "User-Agent": (
277
+ "Mozilla/5.0 (compatible; ScriptoriumAI/1.0; "
278
+ "+https://huggingface.co/spaces/Ma-Ri-Ba-Ku/scriptorium-ai)"
279
+ ),
280
+ "Accept": "image/jpeg,image/png,image/*,*/*",
281
+ "Referer": "https://gallica.bnf.fr/",
282
+ },
283
  follow_redirects=True,
284
  timeout=60.0,
285
  )