From e3e83bb88cf993ff99441a457c6ee1f41049f092 Mon Sep 17 00:00:00 2001 From: Federico Andres Lois Date: Thu, 26 Feb 2026 17:43:47 -0300 Subject: [PATCH] RDBC-1042 URL-decode Database-Missing header before raising DatabaseDoesNotExistException Server sets this header via Uri.EscapeDataString; decode with urllib.parse.unquote before passing to the exception, matching C# Uri.UnescapeDataString behavior. --- ravendb/exceptions/exceptions.py | 2 +- ravendb/http/request_executor.py | 13 +++++- ravendb/tests/issue_tests/test_RDBC_1042.py | 46 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 ravendb/tests/issue_tests/test_RDBC_1042.py diff --git a/ravendb/exceptions/exceptions.py b/ravendb/exceptions/exceptions.py index 47446ffd..6578086e 100644 --- a/ravendb/exceptions/exceptions.py +++ b/ravendb/exceptions/exceptions.py @@ -29,7 +29,7 @@ class ArgumentOutOfRangeException(Exception): pass -class DatabaseDoesNotExistException(Exception): +class DatabaseDoesNotExistException(RuntimeError): pass diff --git a/ravendb/http/request_executor.py b/ravendb/http/request_executor.py index 6e93487b..c319f6d4 100644 --- a/ravendb/http/request_executor.py +++ b/ravendb/http/request_executor.py @@ -6,6 +6,7 @@ import logging import os from concurrent.futures import ThreadPoolExecutor, Future, FIRST_COMPLETED, wait, ALL_COMPLETED +from urllib.parse import unquote import uuid from json import JSONDecodeError from threading import Timer, Semaphore, Lock @@ -564,7 +565,7 @@ def execute( ): db_missing_header = response.headers.get("Database-Missing", None) if db_missing_header is not None: - raise DatabaseDoesNotExistException(db_missing_header) + raise DatabaseDoesNotExistException(unquote(db_missing_header)) self._throw_failed_to_contact_all_nodes(command, request) return # we either handled this already in the unsuccessful response or we are throwing self._on_succeed_request_invoke(self._database_name, url, response, request, attempt_num) @@ -1114,8 +1115,16 @@ def _handle_unsuccessful_response( return True else: command.on_response_failure(response) + db_missing_header = response.headers.get("Database-Missing", None) + if db_missing_header is not None: + raise DatabaseDoesNotExistException(unquote(db_missing_header)) try: # todo: exception dispatcher - raise RuntimeError(json.loads(response.text).get("Message", "Missing message")) + data = json.loads(response.text) + err_type = data.get("Type", "") + message = data.get("Message", "Missing message") + if err_type.endswith("DatabaseDoesNotExistException"): + raise DatabaseDoesNotExistException(message) + raise RuntimeError(message) except JSONDecodeError as e: raise RuntimeError(f"Failed to parse response: {response.text}") from e diff --git a/ravendb/tests/issue_tests/test_RDBC_1042.py b/ravendb/tests/issue_tests/test_RDBC_1042.py new file mode 100644 index 00000000..b8839971 --- /dev/null +++ b/ravendb/tests/issue_tests/test_RDBC_1042.py @@ -0,0 +1,46 @@ +""" +RDBC-1042: Database-Missing header is URL-decoded; raises DatabaseDoesNotExistException. + +C# reference: RavenDB-24435 +""" + +import unittest + +from ravendb.exceptions.exceptions import DatabaseDoesNotExistException +from ravendb.tests.test_base import TestBase + + +class TestUnicodeDatabaseExceptionUnit(unittest.TestCase): + """Unit tests that do not require a live server.""" + + def test_database_does_not_exist_exception_importable(self): + exc = DatabaseDoesNotExistException("my database") + self.assertIn("my database", str(exc)) + + def test_header_unquote_applied(self): + # Simulate the server-set header value (percent-encoded) being decoded + from urllib.parse import unquote + + encoded = "my%20db%20with%20spaces" + exc = DatabaseDoesNotExistException(unquote(encoded)) + self.assertIn("my db with spaces", str(exc)) + self.assertNotIn("%20", str(exc)) + + +class TestUnicodeDatabaseExceptionIntegration(TestBase): + def test_missing_unicode_db_raises_correct_exception(self): + from ravendb.documents.store.definition import DocumentStore + + base_urls = self.get_document_store().urls + with DocumentStore(base_urls, "my db with spaces") as store: + store.initialize() + with self.assertRaises(DatabaseDoesNotExistException) as ctx: + with store.open_session() as session: + session.load("docs/1") + # Exception message should be decoded (no %20) + msg = str(ctx.exception) + self.assertNotIn("%20", msg) + + +if __name__ == "__main__": + unittest.main()