personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: support portal double-serialized user_context and swallowed errors

user_context was JSON-stringified before sending, but the server expects
a native JSON object — causing 400 on every ticket with diagnostics.

Also replaced bare raise_for_status() calls with a helper that includes
the response body in the error message, so server-side validation errors
are surfaced to the user instead of showing only the HTTP status line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+24 -15
+24 -15
apps/support/portal.py
··· 293 293 def _http(self) -> httpx.Client: 294 294 return httpx.Client(timeout=30.0) 295 295 296 + @staticmethod 297 + def _raise_for_status(resp: httpx.Response) -> None: 298 + """Like resp.raise_for_status() but includes the response body.""" 299 + try: 300 + resp.raise_for_status() 301 + except httpx.HTTPStatusError as exc: 302 + detail = resp.text[:500] if resp.text else "" 303 + raise httpx.HTTPStatusError( 304 + f"{exc.request.method} {exc.request.url} — {resp.status_code}: {detail}", 305 + request=exc.request, 306 + response=exc.response, 307 + ) from None 308 + 296 309 def _authed_headers(self, method: str, url: str) -> dict[str, str]: 297 310 """Return Authorization + DPoP headers for an authenticated request.""" 298 311 assert self._access_token is not None ··· 349 362 url = f"{self.portal_url}/tos" 350 363 with self._http() as client: 351 364 resp = client.get(url, headers={"Accept": "text/plain"}) 352 - resp.raise_for_status() 365 + self._raise_for_status(resp) 353 366 tos_text = resp.text 354 367 self._save_tos(tos_text) 355 368 return tos_text ··· 396 409 self._handle = f"{self.handle}-{suffix}" 397 410 return self.register() 398 411 399 - resp.raise_for_status() 412 + self._raise_for_status(resp) 400 413 data = resp.json() 401 414 402 415 self._access_token = data["access_token"] ··· 437 450 if user_email: 438 451 body["user_email"] = user_email 439 452 if user_context: 440 - body["user_context"] = ( 441 - json.dumps(user_context) 442 - if isinstance(user_context, dict) 443 - else user_context 444 - ) 453 + body["user_context"] = user_context 445 454 446 455 resp = self._authed_request("POST", "/api/tickets", json_body=body) 447 - resp.raise_for_status() 456 + self._raise_for_status(resp) 448 457 return resp.json() 449 458 450 459 def list_tickets( ··· 465 474 params["severity"] = severity 466 475 467 476 resp = self._authed_request("GET", "/api/tickets", params=params) 468 - resp.raise_for_status() 477 + self._raise_for_status(resp) 469 478 return resp.json() 470 479 471 480 def get_ticket(self, ticket_id: int) -> dict[str, Any]: 472 481 """Get a single ticket with message thread.""" 473 482 self.ensure_registered() 474 483 resp = self._authed_request("GET", f"/api/tickets/{ticket_id}") 475 - resp.raise_for_status() 484 + self._raise_for_status(resp) 476 485 return resp.json() 477 486 478 487 def reply_to_ticket(self, ticket_id: int, content: str) -> dict[str, Any]: ··· 481 490 resp = self._authed_request( 482 491 "POST", f"/api/tickets/{ticket_id}/messages", json_body={"content": content} 483 492 ) 484 - resp.raise_for_status() 493 + self._raise_for_status(resp) 485 494 return resp.json() 486 495 487 496 # -- Knowledge Base ------------------------------------------------------ ··· 494 503 params["q"] = query 495 504 496 505 resp = self._authed_request("GET", "/api/articles", params=params) 497 - resp.raise_for_status() 506 + self._raise_for_status(resp) 498 507 return resp.json() 499 508 500 509 def get_article(self, slug: str) -> dict[str, Any]: 501 510 """Read a single KB article.""" 502 511 self.ensure_registered() 503 512 resp = self._authed_request("GET", f"/api/articles/{slug}") 504 - resp.raise_for_status() 513 + self._raise_for_status(resp) 505 514 return resp.json() 506 515 507 516 # -- Announcements ------------------------------------------------------- ··· 510 519 """List active announcements.""" 511 520 self.ensure_registered() 512 521 resp = self._authed_request("GET", "/api/announcements") 513 - resp.raise_for_status() 522 + self._raise_for_status(resp) 514 523 return resp.json() 515 524 516 525 # -- Health -------------------------------------------------------------- ··· 519 528 """Check portal health (no auth needed).""" 520 529 with self._http() as client: 521 530 resp = client.get(f"{self.portal_url}/api/health") 522 - resp.raise_for_status() 531 + self._raise_for_status(resp) 523 532 return resp.json() 524 533 525 534