{"record":{"id":"9d31406d8ec67932","repo":"searxng/searxng","slug":"cannot-parse-url","errorCode":null,"errorMessage":"Cannot parse url","messagePattern":"Cannot parse url","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"searx/utils.py","lineNumber":292,"sourceCode":"        * str: normalized URL\n    \"\"\"\n    if url.startswith('//'):\n        # add http or https to this kind of url //example.com/\n        parsed_search_url = urlparse(base_url)\n        url = '{0}:{1}'.format(parsed_search_url.scheme or 'http', url)\n    elif url.startswith('/'):\n        # fix relative url to the search engine\n        url = urljoin(base_url, url)\n\n    # fix relative urls that fall through the crack\n    if '://' not in url:\n        url = urljoin(base_url, url)\n\n    parsed_url = urlparse(url)\n\n    # add a / at this end of the url if there is no path\n    if not parsed_url.netloc:\n        raise ValueError('Cannot parse url')\n    if not parsed_url.path:\n        url += '/'\n\n    return url\n\n\ndef extract_url(xpath_results: list[ElementType] | ElementType | str | Number | bool | None, base_url: str) -> str:\n    \"\"\"Extract and normalize URL from lxml Element\n\n    Example:\n        >>> def f(s, search_url):\n        >>>    return searx.utils.extract_url(html.fromstring(s), search_url)\n        >>> f('<span id=\"42\">https://example.com</span>', 'http://example.com/')\n        'https://example.com/'\n        >>> f('https://example.com', 'http://example.com/')\n        'https://example.com/'\n        >>> f('//example.com', 'http://example.com/')\n        'http://example.com/'","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/searxng/searxng/blob/9fea41204fdfa7a5cfa15b0ebd12904c520478ce/searx/utils.py#L274-L310","documentation":"ValueError('Cannot parse url') raised by normalize_url when, after urljoin with base_url, the resulting URL has an empty netloc (no host). This means the relative url argument could not be resolved to an absolute URL against the engine's base_url.","triggerScenarios":"Calling extract_url/normalize_url with a relative href like '#section', 'javascript:...', or '' against a base_url, or a base_url that is itself malformed so urljoin produces no host.","commonSituations":"Engine site markup changes producing anchor-only or empty links, base_url misconfigured in settings.yml for an engine, or protocol-relative URLs joined against a bad base.","solutions":["Verify the engine's base_url in settings.yml is a full absolute URL with scheme and host","Filter out non-http hrefs before extraction (skip '#', 'javascript:', empty)","Update the engine's XPath selector if the href attribute changed"],"exampleFix":"# before\nurl = extract_url(dom.xpath('//a/@href'), base_url)\n# after\nhrefs = [h for h in dom.xpath('//a/@href') if h and not h.startswith(('#', 'javascript:'))]\nurl = extract_url(hrefs, base_url) if hrefs else None","handlingStrategy":"validation","validationCode":"from urllib.parse import urljoin, urlparse\njoined = urljoin(base_url, url)\nif not urlparse(joined).netloc:\n    return None  # skip unusable link","typeGuard":null,"tryCatchPattern":"try:\n    url = extract_url(nodes, base_url)\nexcept ValueError as e:\n    logger.debug('skipping bad url: %s', e)\n    url = None","preventionTips":["Filter href values before extraction (skip '#', 'javascript:', empty)","Keep engine base_url an absolute URL with scheme+host"],"tags":["searxng","url","engine","parsing"],"backgroundTag":"invalid-url-format","analyzedSha":"9fea41204fdfa7a5cfa15b0ebd12904c520478ce","analyzedAt":"2026-08-27T06:40:00.395Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}