Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions tavily/tavily.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ def __init__(self, api_key: Optional[str] = None, proxies: Optional[dict[str, st
**({"X-Project-ID": tavily_project} if tavily_project else {})
}

self.session = requests.Session()
self.session.headers.update(self.headers)
if self.proxies:
self.session.proxies.update(self.proxies)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared session used concurrently is not thread-safe

Medium Severity

The new requests.Session object is shared across threads in get_company_info, which uses ThreadPoolExecutor to make parallel API calls. requests.Session is not documented as thread-safe, and the previous implementation using independent requests.post() calls was explicitly safe for concurrent use. This change introduces potential race conditions when multiple threads access the session simultaneously.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session pooling introduced without cleanup mechanism

Low Severity

The requests.Session is created and holds connection pools, but there's no close() method, __del__ destructor, or context manager support (__enter__/__exit__) to release these resources. Before this change, each request created and released its own connection. Now resources accumulate until garbage collection. Users creating multiple TavilyClient instances (e.g., in loops or long-running applications) could hit file descriptor limits or experience memory growth. The async client handles this correctly by creating/closing clients per-request with context managers.

Fix in Cursor Fix in Web


def _search(self,
query: str,
search_depth: Literal["basic", "advanced", "fast", "ultra-fast"] = None,
Expand Down Expand Up @@ -91,7 +96,7 @@ def _search(self,
timeout = min(timeout, 120)

try:
response = requests.post(self.base_url + "/search", data=json.dumps(data), headers=self.headers, timeout=timeout, proxies=self.proxies)
response = self.session.post(self.base_url + "/search", data=json.dumps(data), timeout=timeout)
except requests.exceptions.Timeout:
raise TimeoutError(timeout)

Expand Down Expand Up @@ -202,7 +207,7 @@ def _extract(self,
data.update(kwargs)

try:
response = requests.post(self.base_url + "/extract", data=json.dumps(data), headers=self.headers, timeout=timeout, proxies=self.proxies)
response = self.session.post(self.base_url + "/extract", data=json.dumps(data), timeout=timeout)
except requests.exceptions.Timeout:
raise TimeoutError(timeout)

Expand Down Expand Up @@ -310,8 +315,7 @@ def _crawl(self,
data = {k: v for k, v in data.items() if v is not None}

try:
response = requests.post(
self.base_url + "/crawl", data=json.dumps(data), headers=self.headers, timeout=timeout, proxies=self.proxies)
response = self.session.post(self.base_url + "/crawl", data=json.dumps(data), timeout=timeout)
except requests.exceptions.Timeout:
raise TimeoutError(timeout)

Expand Down Expand Up @@ -421,8 +425,7 @@ def _map(self,
data = {k: v for k, v in data.items() if v is not None}

try:
response = requests.post(
self.base_url + "/map", data=json.dumps(data), headers=self.headers, timeout=timeout, proxies=self.proxies)
response = self.session.post(self.base_url + "/map", data=json.dumps(data), timeout=timeout)
except requests.exceptions.Timeout:
raise TimeoutError(timeout)

Expand Down Expand Up @@ -631,12 +634,10 @@ def _research(self,

if stream:
try:
response = requests.post(
response = self.session.post(
self.base_url + "/research",
data=json.dumps(data),
headers=self.headers,
timeout=timeout,
proxies=self.proxies,
stream=True
)
except requests.exceptions.Timeout:
Expand Down Expand Up @@ -671,12 +672,10 @@ def stream_generator() -> Generator[bytes, None, None]:
return stream_generator()
else:
try:
response = requests.post(
response = self.session.post(
self.base_url + "/research",
data=json.dumps(data),
headers=self.headers,
timeout=timeout,
proxies=self.proxies
timeout=timeout
)
except requests.exceptions.Timeout:
raise TimeoutError(timeout)
Expand Down Expand Up @@ -752,11 +751,7 @@ def get_research(self,
dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
"""
try:
response = requests.get(
self.base_url + f"/research/{request_id}",
headers=self.headers,
proxies=self.proxies,
)
response = self.session.get(self.base_url + f"/research/{request_id}")
except Exception as e:
raise Exception(f"Error getting research: {e}")

Expand Down
Loading