@@ -105,13 +105,17 @@ async def _fetch_listing(
105105 while True :
106106 resp = await client .get (url , params = params )
107107 resp .raise_for_status ()
108- payload = resp . json ( )
108+ payload = _json_or_fail ( resp , url )
109109 items .extend (payload .get ("items" , []))
110110 total_count = payload .get ("total_count" , len (items ))
111111 page_info = payload .get ("page_info" ) or {}
112- if limit is not None or not page_info .get ("has_next_page" ):
112+ cursor = page_info .get ("end_cursor" )
113+ # Stop when a single page was requested, when the server reports no more
114+ # pages, or when it claims a next page but gives no cursor to follow
115+ # (guards against an infinite loop re-requesting the same page).
116+ if limit is not None or not page_info .get ("has_next_page" ) or not cursor :
113117 break
114- params = {** params , "cursor" : page_info . get ( "end_cursor" ) }
118+ params = {** params , "cursor" : cursor }
115119 return items , total_count
116120
117121
@@ -170,7 +174,7 @@ async def _run_listing(
170174 except httpx .HTTPError as exc :
171175 error_class = _classify_http_error (exc )
172176 if error_class is _ErrorClass .NOT_FOUND :
173- _fail (error_class , f"Marketplace listing not found on { _host_from (resolved_url )} : { exc } " )
177+ _fail (error_class , f"Marketplace request to { _host_from (resolved_url )} failed : { exc } " )
174178 detail = str (exc ) or type (exc ).__name__
175179 _fail (_ErrorClass .NETWORK , f"Marketplace request failed: { detail } " )
176180 if json_output :
@@ -198,6 +202,19 @@ def _classify_http_error(exc: httpx.HTTPError) -> _ErrorClass:
198202 return _ErrorClass .NETWORK
199203
200204
205+ def _json_or_fail (resp : httpx .Response , source_url : str ) -> Any :
206+ """Parse a JSON response body, failing with a network-class error on invalid JSON.
207+
208+ A 200 with a malformed body is a broken response, not user error, so it is
209+ reported cleanly (exit 2) rather than leaking a raw ``JSONDecodeError`` and
210+ traceback — which would also corrupt ``--json`` output.
211+ """
212+ try :
213+ return resp .json ()
214+ except ValueError :
215+ _fail (_ErrorClass .NETWORK , f"Response from { _host_from (source_url )} is not valid JSON." )
216+
217+
201218def _mkdir_or_fail (path : Path ) -> None :
202219 try :
203220 path .mkdir (parents = True , exist_ok = True )
@@ -422,7 +439,7 @@ async def _fetch_detail(
422439 if resp .status_code == 404 :
423440 _fail (_ErrorClass .NOT_FOUND , f"No collection named '{ namespace } /{ name } ' found on { _host_from (base_url )} ." )
424441 resp .raise_for_status ()
425- return "collection" , resp . json ( )
442+ return "collection" , _json_or_fail ( resp , base_url )
426443
427444 schema_resp , collection_resp = await asyncio .gather (
428445 client .get (_detail_url (base_url , "schema" , namespace , name )),
@@ -435,9 +452,9 @@ async def _fetch_detail(
435452 f"[yellow]Note: '{ namespace } /{ name } ' exists as both a schema and a collection. "
436453 "Resolving as schema. Pass --collection to force the collection path."
437454 )
438- return "schema" , schema_resp . json ( )
455+ return "schema" , _json_or_fail ( schema_resp , base_url )
439456 if isinstance (collection_resp , httpx .Response ) and collection_resp .status_code == 200 :
440- return "collection" , collection_resp . json ( )
457+ return "collection" , _json_or_fail ( collection_resp , base_url )
441458
442459 if _is_transport_failure (schema_resp ) or _is_transport_failure (collection_resp ):
443460 _fail (
@@ -527,7 +544,7 @@ async def show(
527544 except httpx .HTTPError as exc :
528545 error_class = _classify_http_error (exc )
529546 if error_class is _ErrorClass .NOT_FOUND :
530- _fail (error_class , f"Marketplace listing not found on { _host_from ( resolved_url ) } : { exc } " )
547+ _fail (error_class , f"Marketplace request for ' { parsed . namespace } / { parsed . name } ' failed : { exc } " )
531548 message = str (exc ) or type (exc ).__name__
532549 _fail (_ErrorClass .NETWORK , f"Marketplace request failed: { message } " )
533550 if json_output :
0 commit comments