From f9304437267b1113086e3cbec47dfb37e15f1112 Mon Sep 17 00:00:00 2001 From: Patrick Evans Date: Fri, 12 Jun 2026 16:25:13 -0500 Subject: [PATCH] [fix] kagi: safely access unpromised result properties I was testing the new Kagi engine and found for some queries I was getting a `KeyError` exception from the result parsing. This PR ensures we only assumes the `url` key exists, and we use `.get()` to retrieve values for keys that may not be present. Their API documentation [1] clarifies that only the `url` and `title` properties are required/guaranteed in the search result object, but this is not correct: File "/home/patrick/code/searxng/searx/engines/kagi.py", line 161, in response title=html.unescape(result["title"]), ~~~~~~^^^^^^^^^ KeyError: 'title Heard back from Kagi support: > The image results "title' should be marked as optional, as many images likely > don't have titles - as you've noticed. The only required field there should be > the "url" field. [1] https://kagi.redocly.app/api/docs/openapi/search/search#search/search/t=response&c=200&path=data/search [2] https://github.com/searxng/searxng/issues/2247#issuecomment-4692976877 --- searx/engines/kagi.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/searx/engines/kagi.py b/searx/engines/kagi.py index b0c394ea6..63ec64c17 100644 --- a/searx/engines/kagi.py +++ b/searx/engines/kagi.py @@ -42,11 +42,11 @@ To enable Kagi, add the following to the ``engines`` seciton of .. _Api Portal: https://help.kagi.com/kagi/api/overview.html """ - from datetime import datetime, timedelta import typing as t + from searx.extended_types import SXNG_Response from searx.result_types import EngineResults from searx.utils import html_to_text, parse_duration_string @@ -76,7 +76,12 @@ kagi_categ: t.Literal["search", "images", "news", "videos"] = "search" base_url = "https://kagi.com" safe_search_map = {0: False, 1: True, 2: True} -time_range_to_days_map: dict[TimeRangeType, int] = {"day": 1, "week": 7, "month": 30, "year": 365} +time_range_to_days_map: dict[TimeRangeType, int] = { + "day": 1, + "week": 7, + "month": 30, + "year": 365, +} api_key = "" """Kagi API key. Required for using this engine.""" @@ -147,8 +152,8 @@ def response(resp: "SXNG_Response") -> EngineResults: res.add( res.types.MainResult( url=result["url"], - title=html_to_text(result["title"]), - content=html_to_text(result["snippet"]), + title=html_to_text(result.get("title", "no title available")), + content=html_to_text(result.get("snippet", "")), thumbnail=result.get("image", {}).get("url") or "", publishedDate=published_date, ) @@ -157,15 +162,15 @@ def response(resp: "SXNG_Response") -> EngineResults: res.add( res.types.Image( url=result["url"], - title=html_to_text(result.get("title")), + title=html_to_text(result.get("title", "no title available")), img_src=result.get("image", {}).get("url"), - resolution=f"{result['image']['width']}x{result['image']['height']}", + resolution=f"{result.get('image', {}).get('width')}x{result.get('image', {}).get('height')}", thumbnail_src=result.get("props", {}).get("thumbnail", {}).get("url"), ) ) elif kagi_categ == "videos": length: timedelta | None = None - if result["props"].get("duration"): + if result.get("props", {}).get("duration"): length = parse_duration_string(result["props"]["duration"]) res.add( @@ -173,11 +178,11 @@ def response(resp: "SXNG_Response") -> EngineResults: { "template": "videos.html", "url": result["url"], - "title": html_to_text(result["title"]), - "content": html_to_text(result["snippet"]), + "title": html_to_text(result.get("title", "no title available")), + "content": html_to_text(result.get("snippet", "")), "thumbnail": result.get("image", {}).get("url"), "publishedDate": published_date, - "author": result["props"].get("creator_name"), + "author": result.get("props", {}).get("creator_name"), "length": length, } )