@@ -30,6 +30,9 @@ def sdk_version():
3030 return version ("descope" )
3131
3232
33+ # Longest non-JSON body echoed into str()/repr() of a response
34+ _MAX_TEXT_PREVIEW = 200
35+
3336# HTTP status codes that should trigger automatic retries
3437_RETRY_STATUS_CODES = {503 , 520 , 521 , 522 , 524 , 530 }
3538# Delays in seconds between retries: first retry after 100ms, subsequent retries after 5s
@@ -50,6 +53,12 @@ class DescopeResponse:
5053
5154 This allows backward compatibility (acting like a dict) while exposing
5255 HTTP metadata like cf-ray headers for debugging.
56+
57+ Members that need the parsed body (``json()``, ``__getitem__``, ``get``,
58+ ``keys``, ``values``, ``items``, ``__len__``, ``__iter__``, ``__contains__``)
59+ raise on a non-JSON body. Inspecting the response itself never does:
60+ ``bool()`` is always True, and ``str()``/``repr()`` fall back to the raw
61+ text, so a response is always loggable. Use ``is_json`` to check first.
5362 """
5463
5564 def __init__ (self , response : httpx .Response ):
@@ -62,6 +71,15 @@ def json(self):
6271 self ._json_data = self .raw .json ()
6372 return self ._json_data
6473
74+ @property
75+ def is_json (self ) -> bool :
76+ """True if the response body can be parsed as JSON."""
77+ try :
78+ self .json ()
79+ except ValueError :
80+ return False
81+ return True
82+
6583 # Dict-like interface for backward compatibility
6684 def __getitem__ (self , key ):
6785 return self .json ()[key ]
@@ -81,22 +99,43 @@ def items(self):
8199 def get (self , key , default = None ):
82100 return self .json ().get (key , default )
83101
102+ def _text_preview (self ):
103+ """Bounded view of a non-JSON body: its size is upstream-controlled."""
104+ text = self .raw .text
105+ if len (text ) <= _MAX_TEXT_PREVIEW :
106+ return text
107+ return f"{ text [:_MAX_TEXT_PREVIEW ]} ... ({ len (text )} chars, use .text for the full body)"
108+
109+ # Inspection dunders never parse-fail: a non-JSON body (an nginx 502 HTML
110+ # page, for example) must still be loggable and truthy as a response object.
84111 def __str__ (self ):
85- return str (self .json ())
112+ try :
113+ return str (self .json ())
114+ except ValueError :
115+ return self ._text_preview ()
86116
87117 def __repr__ (self ):
88- return f"DescopeResponse({ repr (self .json ())} )"
118+ try :
119+ return f"DescopeResponse({ repr (self .json ())} )"
120+ except ValueError :
121+ return f"DescopeResponse(status_code={ self .raw .status_code } , text={ self ._text_preview ()!r} )"
89122
90123 def __bool__ (self ):
91- return bool (self .json ())
124+ # A response object is always truthy: truthiness answers "did I get a
125+ # response", not "is the body non-empty". Must stay explicit — without
126+ # it Python falls back to __len__, which parses the body.
127+ return True
92128
93129 def __len__ (self ):
94130 return len (self .json ())
95131
96132 def __eq__ (self , other ):
97- if isinstance (other , DescopeResponse ):
98- return self .json () == other .json ()
99- return self .json () == other
133+ try :
134+ if isinstance (other , DescopeResponse ):
135+ return self .json () == other .json ()
136+ return self .json () == other
137+ except ValueError :
138+ return self is other
100139
101140 def __ne__ (self , other ):
102141 return not self .__eq__ (other )
0 commit comments