diff --git a/api/core/rag/extractor/notion_extractor.py b/api/core/rag/extractor/notion_extractor.py index 568ccb1912a..0fc57c830fb 100644 --- a/api/core/rag/extractor/notion_extractor.py +++ b/api/core/rag/extractor/notion_extractor.py @@ -21,6 +21,11 @@ SEARCH_URL = "https://api.notion.com/v1/search" RETRIEVE_PAGE_URL_TMPL = "https://api.notion.com/v1/pages/{page_id}" RETRIEVE_DATABASE_URL_TMPL = "https://api.notion.com/v1/databases/{database_id}" + +# Bounded connect/read timeout so a slow or hanging Notion API cannot block +# dataset extraction indefinitely. +_REQUEST_TIMEOUT = httpx.Timeout(30.0, connect=5.0) + # if user want split by headings, use the corresponding splitter HEADING_SPLITTER = { "heading_1": "# ", @@ -110,6 +115,7 @@ class NotionExtractor(BaseExtractor): "Notion-Version": "2022-06-28", }, json=current_query, + timeout=_REQUEST_TIMEOUT, ) response_data = res.json() @@ -179,6 +185,7 @@ class NotionExtractor(BaseExtractor): "Notion-Version": "2022-06-28", }, params=query_dict, + timeout=_REQUEST_TIMEOUT, ) if res.status_code != 200: raise ValueError(f"Error fetching Notion block data: {res.text}") @@ -241,6 +248,7 @@ class NotionExtractor(BaseExtractor): "Notion-Version": "2022-06-28", }, params=query_dict, + timeout=_REQUEST_TIMEOUT, ) data = res.json() if "results" not in data or data["results"] is None: @@ -301,6 +309,7 @@ class NotionExtractor(BaseExtractor): "Notion-Version": "2022-06-28", }, params=query_dict, + timeout=_REQUEST_TIMEOUT, ) data = res.json() # get table headers text @@ -375,6 +384,7 @@ class NotionExtractor(BaseExtractor): "Notion-Version": "2022-06-28", }, json=query_dict, + timeout=_REQUEST_TIMEOUT, ) data = res.json() diff --git a/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py b/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py index f5d3187828d..8f49647fe7c 100644 --- a/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py +++ b/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py @@ -207,6 +207,26 @@ class TestNotionDatabase: mocker.patch("httpx.post", return_value=_mock_response({"results": None})) assert extractor._get_notion_database_data("db-1") == [] + def test_requests_use_bounded_timeout(self, mocker: MockerFixture): + """All outbound Notion API calls must carry a bounded timeout so a hanging endpoint cannot block extraction.""" + extractor = notion_extractor.NotionExtractor( + notion_workspace_id="ws", + notion_obj_id="obj", + notion_page_type="database", + tenant_id="tenant", + notion_access_token="token", + ) + + mock_post = mocker.patch("httpx.post", return_value=_mock_response({"results": None})) + extractor._get_notion_database_data("db-1") + assert mock_post.call_args.kwargs["timeout"] == notion_extractor._REQUEST_TIMEOUT + + mock_request = mocker.patch( + "httpx.request", return_value=_mock_response({"last_edited_time": "2024-01-01T00:00:00.000Z"}) + ) + extractor.get_notion_last_edited_time() + assert mock_request.call_args.kwargs["timeout"] == notion_extractor._REQUEST_TIMEOUT + def test_get_notion_database_data_requires_access_token(self): extractor = notion_extractor.NotionExtractor( notion_workspace_id="ws",