Summary
On lago-python-client 1.50.0, FindAllChildrenCommandMixin.find_all sends its GET without an
Authorization header, so the twelve public methods that inherit it return 401 against the real
API. Every other mixin in the file passes the header.
Nothing leaks. The header is absent, not misrouted, so the calls simply fail.
PR #385 reported this in March and proposed the two line fix. It was closed unmerged on
2026-04-05. This issue exists because the defect is still in the current PyPI release, and to
add the reproduction and the scope that the PR did not include.
Reproduction
Clean venv, lago-python-client==1.50.0 from PyPI, Python 3.14.0. A loopback HTTP server
records the Authorization header of each request, and one Client makes all seven calls.
client = Client(api_key="PROBE-LAGO-API-KEY", api_url=f"http://127.0.0.1:{PORT}")
| call |
request |
Authorization |
customer_wallets.find_all("cust_1") |
GET /api/v1/customers/cust_1/wallets |
absent |
customer_invoices.find_all("cust_1") |
GET /api/v1/customers/cust_1/invoices |
absent |
plan_charges.find_all("plan_1") |
GET /api/v1/plans/plan_1/charges |
absent |
subscription_charges.find_all("sub_1") |
GET /api/v1/subscriptions/sub_1/charges |
absent |
customers.find_all() |
GET /api/v1/customers |
Bearer PROBE-LAGO-API-KEY |
plan_charges.find("plan_1", "charge_1") |
GET /api/v1/plans/plan_1/charges/charge_1 |
Bearer PROBE-LAGO-API-KEY |
customer_payment_methods.destroy("cust_1", "pm_1") |
DELETE /api/v1/customers/cust_1/payment_methods/pm_1 |
Bearer PROBE-LAGO-API-KEY |
The last three are controls. Row six uses the same client object as row three, so the difference
is the method and not the client. Row seven is a hand written method in
customers/payment_methods_client.py, the same file as one of the affected classes, and it
passes the header explicitly.
Where it is
Twelve of the thirteen mixins in mixins.py build the request like this:
mixins.py:134 api_response: Response = send_get_request(
mixins.py:135 url=make_url(...),
mixins.py:140 headers=make_headers(api_key=self.api_key),
mixins.py:141 timeout=timeout,
mixins.py:142 rate_limit_retry_config=self.rate_limit_retry_config,
mixins.py:143 )
FindAllChildrenCommandMixin.find_all does not:
mixins.py:156 def find_all(
mixins.py:157 self: _ClientMixin[_M],
mixins.py:158 resource_id: str,
mixins.py:159 options: QueryPairs = None,
mixins.py:160 timetour: Optional[httpx.Timeout] = None,
mixins.py:161 ) -> Mapping[str, Any]:
...
mixins.py:166 api_response: Response = send_get_request(
mixins.py:167 url=make_url(...),
mixins.py:176 rate_limit_retry_config=self.rate_limit_retry_config,
mixins.py:177 )
send_get_request is _create_retry_wrapper(httpx.get) in services/request.py:127, which
forwards kwargs to httpx and adds no default headers. Nothing downstream puts it back.
The parameter on line 160 is also spelled timetour, and it is never forwarded. Since
send_get_request wraps httpx.get, these calls fall back to httpx's own 5 second default
rather than the DEFAULT_TIMEOUT of 10 seconds that mixins.py:35 sets for everything else.
Scope
Twelve public methods inherit it:
customer_applied_coupons, customer_credit_notes, customer_invoices,
customer_payment_methods, customer_payment_requests, customer_payments,
customer_subscriptions, customer_wallets, plan_charges, plan_fixed_charges,
subscription_charges, subscription_fixed_charges, each via .find_all(...).
History
The mixin was created already missing the header, in commit 9cdc20e (2025-09-15,
"feat(customers): Add new routes scoped to cusomters", #338). The diff introduces both the
missing headers= and the timetour spelling.
Commit ac96f50 (2026-04-13, "feat: add HTTP 429 rate limit retry support", #390) edited this
same send_get_request(...) call to add rate_limit_retry_config=. The header was not added
then either.
Suggested fix
The same two lines PR #385 proposed:
api_response: Response = send_get_request(
url=make_url(...),
headers=make_headers(api_key=self.api_key),
timeout=timetour,
rate_limit_retry_config=self.rate_limit_retry_config,
)
Renaming timetour to timeout would match the other twelve mixins, though it is a signature
change for anyone passing it by keyword. Keeping both, with timetour deprecated, avoids that.
Reopening #385 would be the shortest path. Happy to open a fresh PR instead if that is easier.
Summary
On lago-python-client 1.50.0,
FindAllChildrenCommandMixin.find_allsends its GET without anAuthorization header, so the twelve public methods that inherit it return 401 against the real
API. Every other mixin in the file passes the header.
Nothing leaks. The header is absent, not misrouted, so the calls simply fail.
PR #385 reported this in March and proposed the two line fix. It was closed unmerged on
2026-04-05. This issue exists because the defect is still in the current PyPI release, and to
add the reproduction and the scope that the PR did not include.
Reproduction
Clean venv,
lago-python-client==1.50.0from PyPI, Python 3.14.0. A loopback HTTP serverrecords the Authorization header of each request, and one
Clientmakes all seven calls.customer_wallets.find_all("cust_1")customer_invoices.find_all("cust_1")plan_charges.find_all("plan_1")subscription_charges.find_all("sub_1")customers.find_all()Bearer PROBE-LAGO-API-KEYplan_charges.find("plan_1", "charge_1")Bearer PROBE-LAGO-API-KEYcustomer_payment_methods.destroy("cust_1", "pm_1")Bearer PROBE-LAGO-API-KEYThe last three are controls. Row six uses the same client object as row three, so the difference
is the method and not the client. Row seven is a hand written method in
customers/payment_methods_client.py, the same file as one of the affected classes, and itpasses the header explicitly.
Where it is
Twelve of the thirteen mixins in
mixins.pybuild the request like this:FindAllChildrenCommandMixin.find_alldoes not:send_get_requestis_create_retry_wrapper(httpx.get)inservices/request.py:127, whichforwards kwargs to httpx and adds no default headers. Nothing downstream puts it back.
The parameter on line 160 is also spelled
timetour, and it is never forwarded. Sincesend_get_requestwrapshttpx.get, these calls fall back to httpx's own 5 second defaultrather than the
DEFAULT_TIMEOUTof 10 seconds that mixins.py:35 sets for everything else.Scope
Twelve public methods inherit it:
customer_applied_coupons,customer_credit_notes,customer_invoices,customer_payment_methods,customer_payment_requests,customer_payments,customer_subscriptions,customer_wallets,plan_charges,plan_fixed_charges,subscription_charges,subscription_fixed_charges, each via.find_all(...).History
The mixin was created already missing the header, in commit 9cdc20e (2025-09-15,
"feat(customers): Add new routes scoped to cusomters", #338). The diff introduces both the
missing
headers=and thetimetourspelling.Commit ac96f50 (2026-04-13, "feat: add HTTP 429 rate limit retry support", #390) edited this
same
send_get_request(...)call to addrate_limit_retry_config=. The header was not addedthen either.
Suggested fix
The same two lines PR #385 proposed:
Renaming
timetourtotimeoutwould match the other twelve mixins, though it is a signaturechange for anyone passing it by keyword. Keeping both, with
timetourdeprecated, avoids that.Reopening #385 would be the shortest path. Happy to open a fresh PR instead if that is easier.