ENG-3255: Include response body in ClientUnsuccessfulException message#7875
ENG-3255: Include response body in ClientUnsuccessfulException message#7875dsill-ethyca wants to merge 6 commits intomainfrom
Conversation
When a SaaS connector request fails with a non-2xx HTTP response, the exception message now includes a truncated response body (max 500 chars) so that diagnostic details like error codes propagate through logs instead of being discarded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
❌ Your patch status has failed because the patch coverage (77.77%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #7875 +/- ##
==========================================
- Coverage 85.07% 85.07% -0.01%
==========================================
Files 627 627
Lines 40780 40788 +8
Branches 4742 4743 +1
==========================================
+ Hits 34694 34700 +6
- Misses 5017 5019 +2
Partials 1069 1069 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review: Include response body in ClientUnsuccessfulException message
The core approach is sound — appending a truncated response body to the exception message improves diagnostic visibility for connector failures. The new unit tests are well-structured and cover the important cases. Two issues should be addressed before merge:
Issues to fix
1. http_connector.py line 101 — improvement is never triggered for that connector path
ClientUnsuccessfulException is raised at line 101 of http_connector.py without passing response=response, even though the response object is in scope. This means the new body-inclusion logic only applies to SaaS/AuthenticatedClient paths where response=response is already correctly forwarded — the HTTP connector path (the one that already logs Pii(response.text) at line 90) gets no benefit. See inline comment.
2. Response body ends up in DB-persisted error messages without PII protection
response.text is embedded raw into the exception message string. Call sites like request_runner_service.py line 1271–1283 build an error_message from exc.args[0] and persist it to the DB via add_error_execution_log. The Pii(str(exc.args[0])) wrapper on the log call no longer functions as a guard because the sensitive body is already baked into the string before Pii sees it. This is inconsistent with how the existing http_connector.py path handles the same body (wrapping it in Pii() before logging). An explicit decision is needed about whether storing truncated external response bodies in execution_log.message is acceptable. See inline comment.
Minor suggestions
- Bare
except Exception: pass— narrowing toexcept (AttributeError, TypeError, UnicodeDecodeError)communicates the actual failure modes being guarded and lets unexpected errors surface. See inline comment. - Test parametrize structure — the
no_responsecase usesNoneas a sentinel and branches inside the test body; parametrizing theresponseargument directly would be slightly cleaner. Minor readability nit. See inline comment. startswithassertions in updated tests — the loosening is correct, but the tests intest_http_oauth_connector.pycould additionally assert that the mocked response body appears in the message, giving regression coverage for the actual body-inclusion path end-to-end.
| try: | ||
| if response is not None and hasattr(response, "text") and response.text: | ||
| body = response.text[: self.MAX_RESPONSE_BODY_LENGTH] | ||
| message = f"{message}: {body}" |
There was a problem hiding this comment.
Response bodies from external connectors are treated as potentially PII-sensitive elsewhere in the codebase — for example, http_connector.py wraps response.text in Pii(...) before logging (line 90 of that file).
By embedding response.text directly into the exception message string here, that text will propagate unmasked through all call sites that use str(exc) or exc.args[0]. In particular, request_runner_service.py line 1271 builds error_message = f"... {exc.args[0]}" and then persists that string to the DB via privacy_request.add_error_execution_log() (line 1278). The Pii(str(exc.args[0])) wrapper on the log call (line 1276) no longer provides meaningful protection because the sensitive body is already baked into the string upstream.
This is worth an explicit decision: is storing truncated external response bodies in execution_log.message (DB-persisted) acceptable? If not, callers that write to add_error_execution_log would need to strip or mask the body portion before passing it.
There was a problem hiding this comment.
What’s the probability of PII being present here?
There was a problem hiding this comment.
Discussing with @adamsachs , it probably shouldn't be here but this is conveniently touched by every saas connection so we're going to be a little more cautious
| ], | ||
| ids=[ | ||
| "includes_response_body", | ||
| "truncates_at_500_chars", |
There was a problem hiding this comment.
The no_response case uses response_text=None as a sentinel and branches on it inside the test body. Since None and a Mock() with .text = None are meaningfully different things, this works — but it slightly obscures intent. A small improvement would be to parametrize response directly (passing None vs a Mock) rather than re-creating the object inside the test. Minor readability nit.
| if response is not None and hasattr(response, "text") and response.text: | ||
| body = response.text[: self.MAX_RESPONSE_BODY_LENGTH] | ||
| message = f"{message}: {body}" | ||
| except Exception: |
There was a problem hiding this comment.
Do you think it’s worth adding a log here?
| try: | ||
| if response is not None and hasattr(response, "text") and response.text: | ||
| body = response.text[: self.MAX_RESPONSE_BODY_LENGTH] | ||
| message = f"{message}: {body}" |
There was a problem hiding this comment.
What’s the probability of PII being present here?
Ticket ENG-3255
Description Of Changes
When a SaaS connector request fails with a non-2xx HTTP response,
ClientUnsuccessfulExceptiononly includes the status code in its message (e.g.,"Client call failed with status code '400'"). The actual response body — which contains actionable diagnostic details likeFUNCTIONALITY_NOT_ENABLED,INVALID_TYPE, orMALFORMED_QUERY— is stored on the exception object but discarded when the exception is converted to a string viastr(e).This makes discovery monitor scan failures and connection test failures very difficult to diagnose. For example, a Salesforce monitor scan failure only shows
"Client call failed with status code '400'"in logs, when the response body contains[{"message":"sObject type 'CustomObject' is not supported.","errorCode":"INVALID_TYPE"}].This PR modifies
ClientUnsuccessfulException.__init__to append a truncated response body (max 500 chars) to the exception message, sostr(e)carries diagnostic detail through all log layers.Code Changes
ClientUnsuccessfulException.__init__incommon_exceptions.pyto extract and appendresponse.text[:500]to the message, with graceful fallback forNoneresponses or missing.textattributestest_http_connector.pyandtest_http_oauth_connector.pyfrom exact string match tostartswithchecksSteps to Confirm
Validated locally by triggering a Salesforce monitor scan with an invalid Tooling API query — confirmed the response body (
INVALID_TYPEerror) now appears in all log layers.Pre-Merge Checklist
CHANGELOG.mdupdated