From 5bfac58af45147a0db82f909aa5b5f7717df35ab Mon Sep 17 00:00:00 2001 From: ff137 Date: Fri, 18 Oct 2024 11:31:57 +0200 Subject: [PATCH] :sparkles: Improve error messages for URL validation Relates to #2243 Signed-off-by: ff137 --- src/marshmallow/validate.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index e4536d88c..aa27136b2 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -7,6 +7,7 @@ from abc import ABC, abstractmethod from itertools import zip_longest from operator import attrgetter +from urllib.parse import urlparse from marshmallow import types from marshmallow.exceptions import ValidationError @@ -210,11 +211,32 @@ def __call__(self, value: str) -> str: if "://" in value: scheme = value.split("://")[0].lower() if scheme not in self.schemes: - raise ValidationError(message) + raise ValidationError( + f"Invalid URL scheme '{scheme}'. " + f"Allowed schemes are: {', '.join(self.schemes)}." + ) regex = self._regex(self.relative, self.absolute, self.require_tld) if not regex.search(value): + if self.require_tld: + try: + # Extract the netloc (hostname and port) + parsed_url = urlparse(value) + hostname = parsed_url.hostname + except (ValueError, TypeError, ): + hostname = None + + if hostname: + # Check if hostname is an IP address + is_ip = re.match(r"\d+\.\d+\.\d+\.\d+", hostname) + # Check if hostname contains a dot (.) + has_tld = "." in hostname + if not is_ip and not has_tld: + raise ValidationError( + "URL must include a top-level domain (e.g., '.com', '.org')." + ) + # Default error message for other failures raise ValidationError(message) return value