Skip to content

Commit a445f09

Browse files
authored
Preserve all headers in response info (#559)
## Problem Response headers were being filtered to exclude timing-dependent headers (`x-envoy-upstream-service-time`, `date`, `x-request-id`) to avoid test flakiness. However, these headers can be useful for debugging, monitoring, and understanding request behavior in production environments. Additionally, the `extract_response_info` function was importing modules on every request and performing unnecessary checks, creating performance overhead for a function that runs on every API call. ## Solution Remove header filtering so all response headers are preserved in `_response_info` for REST, asyncio, and gRPC requests. This provides complete header information while maintaining correct equality comparisons (response dataclasses already exclude `_response_info` from equality checks). Also optimize `extract_response_info` performance by moving imports to module level and removing unnecessary conditional checks. ## Changes ### Response Info Extraction (`response_info.py`) - Removed filtering of timing-dependent headers (`x-envoy-upstream-service-time`, `date`, `x-request-id`) - Optimized value conversion to check string type first (most common case) - Updated documentation to reflect that all headers are now included ### REST API Clients (`api_client.py`, `asyncio_api_client.py`) - Moved `extract_response_info` import to module level to eliminate import overhead on every request - Removed unnecessary `if response_info:` check since the function always returns a dict ## Performance Improvements - Eliminated import overhead by moving imports to module level - Removed unnecessary conditional checks - Optimized type checking order for header value conversion (check string type first) These optimizations are especially beneficial for high-throughput applications making many API calls. ## Usage Example No changes required for users - the API remains the same: ```python from pinecone import Pinecone pc = Pinecone(api_key="your-api-key") index = pc.Index("my-index") # All response headers are now available in _response_info upsert_response = index.upsert(vectors=[...]) print(upsert_response._response_info["raw_headers"]) # Now includes all headers: date, x-envoy-upstream-service-time, # x-request-id, x-pinecone-request-lsn, etc. query_response = index.query(vector=[...]) print(query_response._response_info["raw_headers"]) # Includes all headers from query response ``` ## Testing - All existing unit tests pass (414+ tests) - Integration tests verify response info functionality with all headers included - LSN header extraction tests confirm all headers are preserved - Tests access headers flexibly using `.get("raw_headers", {})`, so they continue to work with additional headers ## Breaking Changes None. This is a transparent improvement with no API changes. Response object equality comparisons are unaffected since `_response_info` already has `compare=False` in all response dataclasses (`QueryResponse`, `UpsertResponse`, `UpdateResponse`, `FetchResponse`, `FetchByMetadataResponse`).
1 parent d391c9a commit a445f09

File tree

4 files changed

+43
-48
lines changed

4 files changed

+43
-48
lines changed

pinecone/openapi_support/api_client.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from .auth_util import AuthUtil
2424
from .serializer import Serializer
25+
from pinecone.utils.response_info import extract_response_info
2526

2627

2728
class ApiClient(object):
@@ -208,16 +209,12 @@ def __call_api(
208209
if return_data is not None:
209210
headers = response_data.getheaders()
210211
if headers:
211-
from pinecone.utils.response_info import extract_response_info
212-
213212
response_info = extract_response_info(headers)
214-
# Attach if response_info exists (may contain raw_headers even without LSN values)
215-
if response_info:
216-
if isinstance(return_data, dict):
217-
return_data["_response_info"] = response_info
218-
else:
219-
# Dynamic attribute assignment on OpenAPI models
220-
setattr(return_data, "_response_info", response_info)
213+
if isinstance(return_data, dict):
214+
return_data["_response_info"] = response_info
215+
else:
216+
# Dynamic attribute assignment on OpenAPI models
217+
setattr(return_data, "_response_info", response_info)
221218

222219
if _return_http_data_only:
223220
return return_data

pinecone/openapi_support/asyncio_api_client.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .serializer import Serializer
2121
from .deserializer import Deserializer
2222
from .auth_util import AuthUtil
23+
from pinecone.utils.response_info import extract_response_info
2324

2425
logger = logging.getLogger(__name__)
2526
""" :meta private: """
@@ -173,16 +174,12 @@ async def __call_api(
173174
if return_data is not None:
174175
headers = response_data.getheaders()
175176
if headers:
176-
from pinecone.utils.response_info import extract_response_info
177-
178177
response_info = extract_response_info(headers)
179-
# Attach if response_info exists (may contain raw_headers even without LSN values)
180-
if response_info:
181-
if isinstance(return_data, dict):
182-
return_data["_response_info"] = response_info
183-
else:
184-
# Dynamic attribute assignment on OpenAPI models
185-
setattr(return_data, "_response_info", response_info)
178+
if isinstance(return_data, dict):
179+
return_data["_response_info"] = response_info
180+
else:
181+
# Dynamic attribute assignment on OpenAPI models
182+
setattr(return_data, "_response_info", response_info)
186183

187184
if _return_http_data_only:
188185
return return_data

pinecone/openapi_support/model_utils.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,15 @@ def __eq__(self, other):
498498
if not isinstance(other, self.__class__):
499499
return False
500500

501-
if not set(self._data_store.keys()) == set(other._data_store.keys()):
501+
# Exclude _response_info from equality comparison since it contains
502+
# timing-dependent headers that may differ between requests
503+
self_keys = {k for k in self._data_store.keys() if k != "_response_info"}
504+
other_keys = {k for k in other._data_store.keys() if k != "_response_info"}
505+
506+
if not self_keys == other_keys:
502507
return False
503-
for _var_name, this_val in self._data_store.items():
508+
for _var_name in self_keys:
509+
this_val = self._data_store[_var_name]
504510
that_val = other._data_store[_var_name]
505511
types = set()
506512
types.add(this_val.__class__)
@@ -653,9 +659,15 @@ def __eq__(self, other):
653659
if not isinstance(other, self.__class__):
654660
return False
655661

656-
if not set(self._data_store.keys()) == set(other._data_store.keys()):
662+
# Exclude _response_info from equality comparison since it contains
663+
# timing-dependent headers that may differ between requests
664+
self_keys = {k for k in self._data_store.keys() if k != "_response_info"}
665+
other_keys = {k for k in other._data_store.keys() if k != "_response_info"}
666+
667+
if not self_keys == other_keys:
657668
return False
658-
for _var_name, this_val in self._data_store.items():
669+
for _var_name in self_keys:
670+
this_val = self._data_store[_var_name]
659671
that_val = other._data_store[_var_name]
660672
types = set()
661673
types.add(this_val.__class__)

pinecone/utils/response_info.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
"""Response information utilities for extracting LSN headers from API responses."""
1+
"""Response information utilities for extracting headers from API responses."""
22

33
from typing import Any, TypedDict
44

5-
# Exclude timing-dependent headers that cause test flakiness
6-
# Defined at module level to avoid recreation on every function call
7-
_TIMING_HEADERS = frozenset(
8-
(
9-
"x-envoy-upstream-service-time",
10-
"date",
11-
"x-request-id", # Request IDs are unique per request
12-
)
13-
)
14-
155

166
class ResponseInfo(TypedDict):
177
"""Response metadata including raw headers.
@@ -26,8 +16,9 @@ class ResponseInfo(TypedDict):
2616
def extract_response_info(headers: dict[str, Any] | None) -> ResponseInfo:
2717
"""Extract raw headers from response headers.
2818
29-
Extracts and normalizes response headers from API responses.
30-
Header names are normalized to lowercase keys.
19+
Extracts and normalizes all response headers from API responses.
20+
Header names are normalized to lowercase keys. All headers are included
21+
without filtering.
3122
3223
Args:
3324
headers: Dictionary of response headers, or None.
@@ -47,21 +38,19 @@ def extract_response_info(headers: dict[str, Any] | None) -> ResponseInfo:
4738
if not headers:
4839
return {"raw_headers": {}}
4940

50-
# Optimized: use dictionary comprehension for better performance
51-
# Pre-compute lowercase keys and filter in one pass
41+
# Optimized: normalize keys to lowercase and convert values to strings
42+
# Check string type first (most common case) for better performance
5243
raw_headers = {}
5344
for key, value in headers.items():
5445
key_lower = key.lower()
55-
if key_lower not in _TIMING_HEADERS:
56-
# Optimize value conversion: check most common types first
57-
if isinstance(value, list) and value:
58-
raw_headers[key_lower] = str(value[0])
59-
elif isinstance(value, tuple) and value:
60-
raw_headers[key_lower] = str(value[0])
61-
elif isinstance(value, str):
62-
# Already a string, no conversion needed
63-
raw_headers[key_lower] = value
64-
else:
65-
raw_headers[key_lower] = str(value)
46+
if isinstance(value, str):
47+
# Already a string, no conversion needed
48+
raw_headers[key_lower] = value
49+
elif isinstance(value, list) and value:
50+
raw_headers[key_lower] = str(value[0])
51+
elif isinstance(value, tuple) and value:
52+
raw_headers[key_lower] = str(value[0])
53+
else:
54+
raw_headers[key_lower] = str(value)
6655

6756
return {"raw_headers": raw_headers}

0 commit comments

Comments
 (0)