summaryrefslogtreecommitdiff
path: root/searx
diff options
context:
space:
mode:
authorBnyro <bnyro@tutanota.com>2026-07-13 14:49:43 +0200
committerBnyro <bnyro@tutanota.com>2026-07-15 17:39:10 +0200
commit5cb4cb2bc524d120b0c1a58b556abf576fb39a06 (patch)
tree3d51e546ee3404af6fa155e590257427b5f692d6 /searx
parent5a448596ab230c0049af4f4da5e427f534840d51 (diff)
[fix] qwant: engine blocked with captcha
Qwant now requires a `datadome` cookie that it returns in the first search response as a `Set-Cookie`. This cookie has to be sent for all requests, otherwise they will be blocked. This means that now, the first search request is blocked (results in CAPTCHA), and only the subsequent searches work (same happens on the Qwant website for me). However, I don't think it's worth repeating the same search request multiple times very quickly because that also makes us more suspicious.
Diffstat (limited to 'searx')
-rw-r--r--searx/engines/qwant.py34
1 files changed, 25 insertions, 9 deletions
diff --git a/searx/engines/qwant.py b/searx/engines/qwant.py
index 1c690146c..88216e885 100644
--- a/searx/engines/qwant.py
+++ b/searx/engines/qwant.py
@@ -51,6 +51,7 @@ from urllib.parse import urlencode
import babel
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
+from searx.enginelib import EngineCache
from searx.enginelib.traits import EngineTraits
from searx.exceptions import (
SearxEngineAccessDeniedException,
@@ -105,9 +106,19 @@ qwant_news_locales = [
]
# fmt: on
+base_url = "https://www.qwant.com"
api_url = "https://api.qwant.com/v3/search/"
"""URL of Qwant's API (JSON)"""
+CACHE: EngineCache
+"""Cache for storing the ``datadome`` cookie."""
+
+
+def setup(engine_settings: dict[str, t.Any]) -> bool:
+ global CACHE # pylint: disable=global-statement
+ CACHE = EngineCache(engine_settings["name"])
+ return True
+
def request(query: str, params: "OnlineParams") -> None:
"""Qwant search request"""
@@ -129,24 +140,29 @@ def request(query: str, params: "OnlineParams") -> None:
"tgp": test_group_value,
"device": "desktop",
"safesearch": params["safesearch"],
- "display": True,
- "llm": True,
+ # True would be encoded to "True", instead of "true", which makes the request
+ # easier to detect and block
+ "displayed": "true",
+ "llm": "true",
}
- # shuffle query parameters to be harder to fingerprint
- args = list(args.items())
- random.shuffle(args)
- args = dict(args)
-
params["raise_for_httperror"] = False
params["url"] = f"{api_url}{qwant_categ}?{urlencode(args)}"
+ params["cookies"]["datadome"] = CACHE.get("datadome")
+ params["headers"].update({"Accept": "application/json", "Referer": f"{base_url}/", "Origin": base_url})
+
def response(resp: "SXNG_Response") -> EngineResults:
"""Parse results from Qwant's API"""
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
+ # cache datadome cookie - changes on each request
+ datadome = resp.cookies.get("datadome")
+ if datadome:
+ CACHE.set("datadome", datadome)
+
res = EngineResults()
# Try to load JSON result
@@ -163,8 +179,8 @@ def response(resp: "SXNG_Response") -> EngineResults:
error_code = data.get("error_code")
if error_code == 24:
raise SearxEngineTooManyRequestsException()
- if search_results.get("data", {}).get("error_data", {}).get("captchaUrl") is not None:
- raise SearxEngineCaptchaException()
+ if search_results.get("url") is not None:
+ raise SearxEngineCaptchaException(suspended_time=0)
if resp.status_code == 403:
raise SearxEngineAccessDeniedException()
msg = ",".join(data.get("message", ["unknown"]))