Fix 403 on test-woo: add User-Agent header, classify 403 as WooForbiddenError
WordPress security plugins (Wordfence, WAF) block the default Python-urllib User-Agent with 403. Send a descriptive User-Agent on all Woo requests. Also add WooForbiddenError so 403 is classified distinctly from 401 (auth) per SETTINGS-80, and surface a clear message in test-woo.
This commit is contained in:
@@ -191,6 +191,7 @@ from .woo import (
|
||||
WooConnectionError,
|
||||
WooCredentialsMissing,
|
||||
WooError,
|
||||
WooForbiddenError,
|
||||
)
|
||||
|
||||
|
||||
@@ -218,6 +219,8 @@ async def test_woo() -> dict[str, Any]:
|
||||
result = taxonomy_mod.sync(client, get_connection())
|
||||
except WooAuthError as e:
|
||||
return {"ok": False, "detail": f"auth rejected by store: {e}", "taxonomy_anchor": None, "taxonomy_last_sync_at": None}
|
||||
except WooForbiddenError as e:
|
||||
return {"ok": False, "detail": f"request blocked by store (403): {e}", "taxonomy_anchor": None, "taxonomy_last_sync_at": None}
|
||||
except WooConnectionError as e:
|
||||
return {"ok": False, "detail": f"store unreachable: {e}", "taxonomy_anchor": None, "taxonomy_last_sync_at": None}
|
||||
except WooError as e:
|
||||
|
||||
@@ -14,6 +14,7 @@ from backend.woo import (
|
||||
WooClient,
|
||||
WooConnectionError,
|
||||
WooCredentialsMissing,
|
||||
WooForbiddenError,
|
||||
WooServerError,
|
||||
)
|
||||
from backend import taxonomy as taxonomy_mod
|
||||
@@ -138,3 +139,25 @@ def test_anchor_stable_across_identical_syncs(db_dir):
|
||||
c2, _ = _client([cats, tags, attrs])
|
||||
r2 = taxonomy_mod.sync(c2, db_mod.get_connection())
|
||||
assert r1["taxonomy_anchor"] == r2["taxonomy_anchor"]
|
||||
|
||||
|
||||
def test_403_raises_forbidden_error(db_dir):
|
||||
def fake_request(url, method, data, headers):
|
||||
raise WooForbiddenError("request forbidden (403)")
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
with pytest.raises(WooForbiddenError):
|
||||
c.get_categories()
|
||||
|
||||
|
||||
def test_requests_include_user_agent(db_dir):
|
||||
seen_headers = {}
|
||||
|
||||
def fake_request(url, method, data, headers):
|
||||
seen_headers.update(headers)
|
||||
return 200, {"X-WP-TotalPages": "1"}, b"[]"
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
c.get_categories()
|
||||
assert "User-Agent" in seen_headers
|
||||
assert "SlabUploader" in seen_headers["User-Agent"]
|
||||
|
||||
+16
-1
@@ -13,6 +13,10 @@ from typing import Any, Callable
|
||||
|
||||
from .crypto import DecryptError, decrypt
|
||||
|
||||
# WordPress security plugins (Wordfence, WAF, CDN) commonly block the default
|
||||
# Python-urllib User-Agent with 403. Send a descriptive one.
|
||||
_USER_AGENT = "SlabUploader/1.0 (+https://www.whidbeywoodstore.com)"
|
||||
|
||||
|
||||
class WooError(Exception):
|
||||
"""Base for all Woo client failures."""
|
||||
@@ -26,6 +30,10 @@ class WooAuthError(WooError):
|
||||
"""The store rejected the credentials (401)."""
|
||||
|
||||
|
||||
class WooForbiddenError(WooError):
|
||||
"""The store blocked the request (403) — usually a security plugin or WAF."""
|
||||
|
||||
|
||||
class WooNotFoundError(WooError):
|
||||
pass
|
||||
|
||||
@@ -54,6 +62,8 @@ def _default_request(url: str, method: str, data: bytes | None, headers: dict[st
|
||||
msg = parsed.get("message") if isinstance(parsed, dict) else None
|
||||
if e.code == 401:
|
||||
raise WooAuthError(msg or "authentication rejected (401)") from e
|
||||
if e.code == 403:
|
||||
raise WooForbiddenError(msg or "request forbidden (403)") from e
|
||||
if e.code == 404:
|
||||
raise WooNotFoundError(msg or "not found (404)") from e
|
||||
raise WooServerError(f"server error {e.code}: {msg}") from e
|
||||
@@ -90,7 +100,11 @@ class WooClient:
|
||||
url = f"{self.base_url}/wp-json/wc/v3{path}"
|
||||
return self._request(
|
||||
url, "GET", None,
|
||||
{"Authorization": f"Basic {self._auth}", "Accept": "application/json"},
|
||||
{
|
||||
"Authorization": f"Basic {self._auth}",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": _USER_AGENT,
|
||||
},
|
||||
)
|
||||
|
||||
def _fetch_all(self, path: str) -> list[dict[str, Any]]:
|
||||
@@ -132,6 +146,7 @@ class WooClient:
|
||||
{
|
||||
"Authorization": f"Basic {self._auth}",
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
"User-Agent": _USER_AGENT,
|
||||
},
|
||||
)
|
||||
if code not in (200, 201):
|
||||
|
||||
Reference in New Issue
Block a user