Skip to content
Merged
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
27 changes: 27 additions & 0 deletions tests/test_sparql_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@ def test_sparql_fetch_http_error():
source.fetch("https://example.test/sparql", "SELECT * WHERE { }")


def test_sparql_fetch_post_403_falls_back_to_get():
"""POST 403 (WAF/Virtuoso) deve cadere sul fallback GET.

Regressione: il plugin considerava ok il 403 (is_ok = response presente,
err None) e salvava il body HTML come CSV — il fallback GET non scattava.
Caso reale: dati.senato.it accetta solo GET (POST → 403).
"""
with patch("toolkit.plugins.sparql.HttpClient") as mock_cls:
mock_cls.return_value.post.return_value = _http_ok(
status=403,
text="<!DOCTYPE html><html>Forbidden</html>",
)
mock_cls.return_value.get.return_value = _http_ok(
status=200,
text="name,value\nfoo,123\n",
headers={"Content-Type": "text/csv"},
)
source = SparqlSource()
payload, origin = source.fetch(
"https://dati.senato.it/sparql",
"SELECT ?name ?value WHERE { }",
accept_format="csv",
)
assert b"foo" in payload
assert mock_cls.return_value.get.called # il fallback GET è scattato


def test_sparql_fetch_network_error():
"""Network error raises DownloadError."""
with patch("toolkit.plugins.sparql.HttpClient") as mock_cls:
Expand Down
43 changes: 31 additions & 12 deletions toolkit/plugins/sparql.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,39 @@ def _do_fetch(self, endpoint: str, q: str, accept_format: str) -> bytes:
headers=headers,
retries=2,
)
if result.is_ok:
# is_ok è True anche su errori HTTP (response presente, err None) —
# ma una risposta 4xx/5xx NON è un risultato SPARQL valido.
# Fallback GET solo su 4xx (WAF/Virtuoso rifiutano POST): 403 è il
# caso tipico. Su 5xx il fallback non deve mascherare l'errore server.
post_status = (
result.response.status_code if result.is_ok and result.response is not None else None
)
if post_status is not None and post_status < 400:
return self._parse_response(result.response, is_json)

# --- Tentativo 2: GET fallback ---
url = f"{endpoint}?query={urllib.parse.quote(q)}"
get_headers = {
"Accept": (
"application/sparql-results+xml,"
"application/sparql-results+json,application/json,text/csv"
),
}
result = self._client.get(url, headers=get_headers)
if result.is_ok:
return self._parse_response(result.response, is_json)
# --- Tentativo 2: GET fallback (solo se POST è fallito con 4xx) ---
if post_status is not None and 400 <= post_status < 500:
url = f"{endpoint}?query={urllib.parse.quote(q)}"
get_headers = {
"Accept": (
"application/sparql-results+xml,"
"application/sparql-results+json,application/json,text/csv"
),
}
result = self._client.get(url, headers=get_headers)
get_status = (
result.response.status_code
if result.is_ok and result.response is not None
else None
)
if get_status is not None and get_status < 400:
return self._parse_response(result.response, is_json)

if post_status is not None and post_status >= 500:
body = (result.response.text or "")[:200] if result.response is not None else ""
raise DownloadError(
f"SPARQL endpoint returned HTTP {post_status} for {endpoint}: {body}"
)

raise DownloadError(
f"SPARQL request failed for {endpoint}: POST → {result.err or 'unknown'}"
Expand Down
Loading