-
Notifications
You must be signed in to change notification settings - Fork 155
Bug: RESTResponse.data is None after ApiClient.call_api despite successful aiohttp response and body read #6
Description
Environment:
- OS: Linux
- Python Version: 3.11.9
lighter-pythonSDK Source: Installed viapip install git+https://github.com/elliottech/lighter-python.git
Problem Description:
When using the generic ApiClient.call_api method to make a GET request to an API endpoint known to return valid JSON data, the returned lighter.rest.RESTResponse object incorrectly has its data attribute set to None. This occurs even when the underlying aiohttp request receives a successful HTTP 200 status code and contains a valid response body, which can be confirmed by reading the aiohttp.ClientResponse directly.
This prevents the successful processing and deserialization of responses obtained through ApiClient.call_api, forcing the use of workarounds like direct aiohttp calls for endpoints that may not have dedicated high-level SDK methods (e.g., GET /api/v1/positionFunding).
Steps to Reproduce:
The following minimal script demonstrates the issue using the public /api/v1/orderBookDetails endpoint (which is known to work via the dedicated OrderApi.order_book_details method):
import asyncio
import json
from typing import Optional
import lighter
from lighter import rest
from lighter.configuration import Configuration
from lighter.api_client import ApiClient
TESTNET_URL = "https://testnet.zklighter.elliot.ai"
ENDPOINT_PATH = "/api/v1/orderBookDetails"
QUERY_PARAMS = "market_id=0"
async def run_test():
print(f"Testing generic call_api to: {ENDPOINT_PATH}?{QUERY_PARAMS}")
config = Configuration(host=TESTNET_URL)
api_client = ApiClient(configuration=config)
response_obj: Optional[rest.RESTResponse] = None # Use Optional for clarity
try:
full_url = f"{config.host}{ENDPOINT_PATH}?{QUERY_PARAMS}"
print(f"Calling api_client.call_api with URL: {full_url}")
response_obj = await api_client.call_api(
method="GET",
url=full_url,
header_params={"Accept": "application/json"}
)
print(f"call_api response status: {response_obj.status if response_obj else 'N/A'}")
print(f"call_api response object type: {type(response_obj)}")
if response_obj:
print(f"call_api response.data is None: {response_obj.data is None}") # <-- EXPECTED: False, ACTUAL: True
if response_obj.data is not None:
print("SUCCESS: response.data is populated.")
else:
print("FAILURE: response.data is None, cannot decode/process.")
# This branch is incorrectly hit
except Exception as e:
print(f"An error occurred: {e}")
import traceback
traceback.print_exc()
finally:
if api_client:
await api_client.close()
if __name__ == "__main__":
asyncio.run(run_test())Expected Behavior:
When the script is run:
- The
api_client.call_apirequest should succeed with HTTP status 200. - The output line
call_api response.data is None:should printFalse. - The script should print "SUCCESS: response.data is populated."
Actual Behavior:
When the script is run:
- The
api_client.call_apirequest succeeds with HTTP status 200. - The output line
call_api response.data is None:printsTrue. - The script prints "FAILURE: response.data is None, cannot decode/process.".
- Any subsequent attempt to use
response_obj.data(e.g.,response_obj.data.decode()) would raise anAttributeError: 'NoneType' object has no attribute 'decode'.
Debugging Information
Further debugging by modifying the SDK's lighter/rest.py file (specifically the RESTClientObject.request method) confirmed that:
- The internal
aiohttp.ClientResponseobject (r) successfully receives the HTTP 200 status and headers. - Calling
await r.read()on the internalaiohttp.ClientResponseobject successfully returns the expected JSON response body as bytes. - However, the
lighter.rest.RESTResponseobject created immediately after (viaRESTResponse(r)) has itsdataattribute set toNone.
My guess is that the issue lies within the lighter.rest.RESTResponse.__init__ method, where the response body from the aiohttp object is not being correctly read or assigned to the self.data attribute of the RESTResponse instance when the response is generated via the call_api path.
Standard SDK methods (like OrderApi.order_book_details) work correctly, suggesting they might use a different internal request/response handling path that avoids this specific issue in the RESTResponse object's initialization or usage via call_api.
Workaround:
For endpoints without dedicated SDK methods (like /api/v1/positionFunding), a successful workaround involves bypassing ApiClient.call_api and using aiohttp.ClientSession directly to make the request and process the response body via await response.read().
Suggestion:
Please investigate the lighter.rest.RESTResponse.__init__ method to ensure it correctly populates the data attribute from the provided aiohttp.ClientResponse object under all circumstances, including when invoked via the ApiClient.call_api pathway.