-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fixes #24348: Strip URL scheme from hostPort to prevent ValueError #27191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bf850b3
267c821
1623135
2a95020
96428c1
d813bd6
ac81679
bf602bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Callable, Dict, Literal, Optional, Union | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from urllib.parse import urlparse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel as PydanticBaseModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import WrapSerializer, model_validator | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,6 +36,101 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON_ENCODERS = "json_encoders" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def strip_hostport_scheme(raw: str) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 39 in ingestion/src/metadata/ingestion/models/custom_pydantic.py
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Strip an accidental URL scheme from a hostPort string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Self-contained helper that depends only on the standard library so it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| can be imported from ``builders.py`` and ``db_utils.py`` without circular | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imports. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Raises ValueError if the value cannot be resolved to a valid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``hostname[:port]`` string — e.g. when the port is non-numeric or when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the hostname is empty after stripping the scheme. This applies to both | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| standard URLs handled by ``urlparse`` and to JDBC-style URLs handled by | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the fallback branch (e.g. ``jdbc:postgresql://host:abc/db``). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value = raw.strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "://" not in value: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsed = urlparse(value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname = parsed.hostname or "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Build a sanitised label for log/error messages that identifies the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # problematic input without leaking credentials or a full path. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if parsed.scheme and hostname: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safe_label = f"{parsed.scheme}://{hostname}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # urlparse couldn't extract a hostname (e.g. jdbc:postgresql://…). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Construct the label from the raw tail so messages remain actionable. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail_for_label = value.rsplit("://", 1)[-1].split("/", 1)[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "@" in tail_for_label: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail_for_label = tail_for_label.rsplit("@", 1)[-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safe_label = tail_for_label or value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "The hostPort '%s' contains a URL scheme. Expected format is " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "'hostname[:port]' (e.g. 'localhost:3306'). Stripping the scheme prefix.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safe_label, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port = parsed.port | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except ValueError as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid hostPort '{safe_label}'. Expected format is " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "'hostname[:port]' (e.g. 'localhost:3306')." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) from exc | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not hostname: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # urlparse couldn't extract a hostname (e.g. 'jdbc:postgresql://host:5432/db'). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fall back to stripping the scheme and any trailing path/query/fragment/userinfo. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail = value.rsplit("://", 1)[-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for sep in ("/", "?", "#"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail = tail.split(sep, 1)[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "@" in tail: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail = tail.rsplit("@", 1)[-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail = tail.rsplit("@", 1)[-1] | |
| tail = tail.rsplit("@", 1)[-1] | |
| fallback_port = None | |
| if tail.startswith("["): | |
| closing_bracket = tail.find("]") | |
| if closing_bracket != -1 and len(tail) > closing_bracket + 1: | |
| if tail[closing_bracket + 1] == ":": | |
| fallback_port = tail[closing_bracket + 2 :] | |
| elif ":" in tail: | |
| _, fallback_port = tail.rsplit(":", 1) | |
| if fallback_port and not fallback_port.isdigit(): | |
| raise ValueError( | |
| f"Invalid hostPort '{safe_label}'. Expected format is " | |
| "'hostname[:port]' (e.g. 'localhost:3306')." | |
| ) |
Copilot
AI
Apr 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the hostname-empty fallback branch, malformed inputs like http:// or http:///localhost:3306 will produce an empty tail and be returned as an empty hostPort, which can lead to confusing downstream errors. It would be safer to validate that the extracted tail is non-empty (and ideally contains a hostname) and raise the same ValueError if it can't be parsed into hostname[:port].
| # Validate the port in the fallback path so the same ValueError | |
| # contract holds for JDBC-style URLs (e.g. 'jdbc:postgresql://host:abc'). | |
| fallback_port: Optional[str] = None | |
| if tail.startswith("["): | |
| closing = tail.find("]") | |
| if closing != -1 and len(tail) > closing + 1 and tail[closing + 1] == ":": | |
| fallback_port = tail[closing + 2 :] | |
| elif ":" in tail: | |
| fallback_port = tail.rsplit(":", 1)[1] | |
| if fallback_port and not fallback_port.isdigit(): | |
| raise ValueError( | |
| f"Invalid hostPort '{safe_label}'. Expected format is " | |
| "'hostname[:port]' (e.g. 'localhost:3306')." | |
| ) | |
| if not tail: | |
| raise ValueError( | |
| f"Invalid hostPort '{safe_label}'. Expected format is " | |
| "'hostname[:port]' (e.g. 'localhost:3306')." | |
| ) | |
| # Validate the fallback path so the same ValueError contract holds for | |
| # JDBC-style URLs and malformed inputs that urlparse could not map to | |
| # a standard hostname. | |
| fallback_parsed = urlparse(f"//{tail}") | |
| try: | |
| fallback_hostname = fallback_parsed.hostname or "" | |
| _ = fallback_parsed.port | |
| except ValueError as exc: | |
| raise ValueError( | |
| f"Invalid hostPort '{safe_label}'. Expected format is " | |
| "'hostname[:port]' (e.g. 'localhost:3306')." | |
| ) from exc | |
| if not fallback_hostname: | |
| raise ValueError( | |
| f"Invalid hostPort '{safe_label}'. Expected format is " | |
| "'hostname[:port]' (e.g. 'localhost:3306')." | |
| ) |
Check warning on line 172 in ingestion/src/metadata/ingestion/models/custom_pydantic.py
GitHub Actions / Unit Tests & Static Checks (3.10)
Type annotation for attribute `hostPort` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
Uh oh!
There was an error while loading. Please reload this page.