errors.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from __future__ import annotations
  2. import logging
  3. import sys
  4. from typing import Any, Literal, cast
  5. import httpx
  6. import orjson
  7. logger = logging.getLogger(__name__)
  8. class LangGraphError(Exception):
  9. pass
  10. class APIError(httpx.HTTPStatusError, LangGraphError):
  11. message: str
  12. request: httpx.Request
  13. body: object | None
  14. code: str | None
  15. param: str | None
  16. type: str | None
  17. def __init__(
  18. self,
  19. message: str,
  20. response_or_request: httpx.Response | httpx.Request,
  21. *,
  22. body: object | None,
  23. ) -> None:
  24. if isinstance(response_or_request, httpx.Response):
  25. req = response_or_request.request
  26. response = response_or_request
  27. else:
  28. req = response_or_request
  29. response = None
  30. httpx.HTTPStatusError.__init__(self, message, request=req, response=response) # type: ignore[arg-type]
  31. LangGraphError.__init__(self)
  32. self.request = req
  33. self.message = message
  34. self.body = body
  35. if isinstance(body, dict):
  36. b = cast("dict[str, Any]", body)
  37. # Best-effort extraction of common fields if present
  38. code_val = b.get("code")
  39. self.code = code_val if isinstance(code_val, str) else None
  40. param_val = b.get("param")
  41. self.param = param_val if isinstance(param_val, str) else None
  42. t = b.get("type")
  43. self.type = t if isinstance(t, str) else None
  44. else:
  45. self.code = None
  46. self.param = None
  47. self.type = None
  48. class APIResponseValidationError(APIError):
  49. response: httpx.Response
  50. status_code: int
  51. def __init__(
  52. self,
  53. response: httpx.Response,
  54. body: object | None,
  55. *,
  56. message: str | None = None,
  57. ) -> None:
  58. super().__init__(
  59. message or "Data returned by API invalid for expected schema.",
  60. response,
  61. body=body,
  62. )
  63. self.response = response
  64. self.status_code = response.status_code
  65. class APIStatusError(APIError):
  66. response: httpx.Response
  67. status_code: int
  68. request_id: str | None
  69. def __init__(
  70. self, message: str, *, response: httpx.Response, body: object | None
  71. ) -> None:
  72. super().__init__(message, response, body=body)
  73. self.response = response
  74. self.status_code = response.status_code
  75. self.request_id = response.headers.get("x-request-id")
  76. class APIConnectionError(APIError):
  77. def __init__(
  78. self, *, message: str = "Connection error.", request: httpx.Request
  79. ) -> None:
  80. super().__init__(message, response_or_request=request, body=None)
  81. class APITimeoutError(APIConnectionError):
  82. def __init__(self, request: httpx.Request) -> None:
  83. super().__init__(message="Request timed out.", request=request)
  84. class BadRequestError(APIStatusError):
  85. status_code: Literal[400] = 400
  86. class AuthenticationError(APIStatusError):
  87. status_code: Literal[401] = 401
  88. class PermissionDeniedError(APIStatusError):
  89. status_code: Literal[403] = 403
  90. class NotFoundError(APIStatusError):
  91. status_code: Literal[404] = 404
  92. class ConflictError(APIStatusError):
  93. status_code: Literal[409] = 409
  94. class UnprocessableEntityError(APIStatusError):
  95. status_code: Literal[422] = 422
  96. class RateLimitError(APIStatusError):
  97. status_code: Literal[429] = 429
  98. class InternalServerError(APIStatusError):
  99. pass
  100. def _extract_error_message(body: object | None, fallback: str) -> str:
  101. if isinstance(body, dict):
  102. b = cast("dict[str, Any]", body)
  103. for key in ("message", "detail", "error"):
  104. val = b.get(key)
  105. if isinstance(val, str) and val:
  106. return val
  107. # Sometimes errors are structured like {"error": {"message": "..."}}
  108. err = b.get("error")
  109. if isinstance(err, dict):
  110. e = cast("dict[str, Any]", err)
  111. for key in ("message", "detail"):
  112. val = e.get(key)
  113. if isinstance(val, str) and val:
  114. return val
  115. return fallback
  116. async def _adecode_error_body(r: httpx.Response) -> object | None:
  117. try:
  118. data = await r.aread()
  119. except Exception:
  120. return None
  121. if not data:
  122. return None
  123. try:
  124. return orjson.loads(data)
  125. except Exception:
  126. try:
  127. return data.decode()
  128. except Exception:
  129. return None
  130. def _decode_error_body(r: httpx.Response) -> object | None:
  131. try:
  132. data = r.read()
  133. except Exception:
  134. return None
  135. if not data:
  136. return None
  137. try:
  138. return orjson.loads(data)
  139. except Exception:
  140. try:
  141. return data.decode()
  142. except Exception:
  143. return None
  144. def _map_status_error(response: httpx.Response, body: object | None) -> APIStatusError:
  145. status = response.status_code
  146. reason = response.reason_phrase or "HTTP Error"
  147. message = _extract_error_message(body, f"{status} {reason}")
  148. if status == 400:
  149. return BadRequestError(message, response=response, body=body)
  150. if status == 401:
  151. return AuthenticationError(message, response=response, body=body)
  152. if status == 403:
  153. return PermissionDeniedError(message, response=response, body=body)
  154. if status == 404:
  155. return NotFoundError(message, response=response, body=body)
  156. if status == 409:
  157. return ConflictError(message, response=response, body=body)
  158. if status == 422:
  159. return UnprocessableEntityError(message, response=response, body=body)
  160. if status == 429:
  161. return RateLimitError(message, response=response, body=body)
  162. if status >= 500:
  163. return InternalServerError(message, response=response, body=body)
  164. return APIStatusError(message, response=response, body=body)
  165. async def _araise_for_status_typed(r: httpx.Response) -> None:
  166. if r.status_code < 400:
  167. return
  168. body = await _adecode_error_body(r)
  169. err = _map_status_error(r, body)
  170. # Log for older Python versions without Exception notes
  171. if not (sys.version_info >= (3, 11)):
  172. logger.error(f"Error from langgraph-api: {getattr(err, 'message', '')}")
  173. raise err
  174. def _raise_for_status_typed(r: httpx.Response) -> None:
  175. if r.status_code < 400:
  176. return
  177. body = _decode_error_body(r)
  178. err = _map_status_error(r, body)
  179. if not (sys.version_info >= (3, 11)):
  180. logger.error(f"Error from langgraph-api: {getattr(err, 'message', '')}")
  181. raise err