Skip to content

Expose structured OperationOutcome issues on client errors ( python and typescript )#384

Open
sunvenu wants to merge 3 commits into
mainfrom
sunanda/expose_operationoutcome_issues
Open

Expose structured OperationOutcome issues on client errors ( python and typescript )#384
sunvenu wants to merge 3 commits into
mainfrom
sunanda/expose_operationoutcome_issues

Conversation

@sunvenu
Copy link
Copy Markdown
Contributor

@sunvenu sunvenu commented Apr 17, 2026

Description of Changes

Briefly describe the change, including justification for the change, ramifications for not doing the change, and any specific versions/configurations impacted.

When the API returns a 400 with a FHIR OperationOutcome, SDK callers previously could only read a formatted string message. This PR will expose the parsed structure as first-class properties.

Changes (Python + TypeScript):

New public type OperationOutcomeIssue with severity, code, diagnostics, details
OrchestrateClientError now exposes .issues, .response_text/.responseText, .status_code/.statusCode
status_code/statusCode defaults to 0 — no breaking change for callers constructing the exception directly
Exception message string format unchanged for existing catch/except code
C#: No changes — already exposed OperationOutcome (typed FHIR model) and Issues on OrchestrateClientException.

Issue Link

This PR addresses the following issues:

  • LINK TO RELEVANT ISSUES, IF APPLICABLE

Security

REMINDER: All file contents are public.

  • I have ensured no secure credentials or sensitive information remain in code, metadata, comments, etc. Of particular note: No temporary testing changes committed such as API base URLs, access tokens, print/log statements, etc.
  • My changes do not introduce any security risks, or any such risks have been properly mitigated.

Describe briefly what security risks you considered, why they don't apply, or how they've been mitigated.

Change Control Board (CCB) Approval

CCB approval is required when the change affects organizational processes (like vulnerability management or disaster recovery), or has the potential to impact availability, security, or privacy. See the CR process for more detailed assessment guidelines.

  • This change does NOT require CCB approval.
  • This change DOES require CCB approval. Tag @careevolution/ccb.

Testing/Validation

Describe how we will know if the change is working as intended. Consider both pre-production testing and post-implementation validation, including environment/configuration details, if applicable.

TODO: REPLACE WITH YOUR TESTING/VALIDATION PLAN

Backout Plan

Describe how we will restore the system to its pre-change state if something goes wrong.

TODO: REPLACE WITH YOUR BACKOUT PLAN

Implementation Notes

Describe any special considerations for implementing the change: scheduling constraints, down-time planning, if a co-pilot is needed, etc.

TODO: REPLACE WITH IMPLEMENTATION NOTES

Documentation Updates

Describe any necessary updates. Consider both internal (wiki, checklists) and external (user guides, developer docs, customer communications) documentation.

  • This change has no documentation impact.
  • This change impacts external documentation (API docs, user guides). Tag @careevolution/mdhd-docs or @careevolution/api-docs.
  • This change impacts internal documentation (procedures, security plans, wiki). Tag the owner.

TODO: REPLACE WITH A DESCRIPTION OF DOCUMENTATION UPDATES, if applicable

Reviewers

  • I have assigned the appropriate reviewer(s).

Minimally, a second set of eyes is needed ensure no non-public information is published. Consider also including subject-matter experts and editing/style reviewers.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves client-side error handling by exposing structured FHIR OperationOutcome.issue information (severity/code/diagnostics/details) on client errors in the Python and TypeScript SDKs, alongside preserving access to raw response text and status code.

Changes:

  • TypeScript: Introduces OperationOutcomeIssue and updates OrchestrateClientError to expose issues, responseText, and statusCode; updates HTTP error parsing to populate these fields.
  • Python: Adds OperationOutcomeIssue and updates OrchestrateClientError to expose issues, response_text, and status_code; updates HTTP error parsing accordingly.
  • Adds new unit tests in TypeScript and Python covering multi-issue parsing, details/diagnostics extraction, and status/response preservation; adds a .NET test to validate multi-issue behavior (no .NET product changes).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
typescript/tests/errorParsing.test.ts Adds vitest coverage for structured issue parsing and new error properties.
typescript/src/httpHandler.ts Switches error parsing to produce structured OperationOutcomeIssue[] and passes status code into the exception.
typescript/src/exceptions.ts Introduces OperationOutcomeIssue and expands OrchestrateClientError surface area.
python/tests/test_error_parsing.py Adds pytest coverage for structured issue parsing and new error properties.
python/orchestrate/exceptions.py Re-exports OperationOutcomeIssue from the public exceptions module.
python/orchestrate/_internal/http_handler.py Switches error parsing to produce structured OperationOutcomeIssue[] and passes status code into the exception.
python/orchestrate/_internal/exceptions.py Adds OperationOutcomeIssue and expands OrchestrateClientError surface area.
dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs Adds a regression test asserting multi-issue exposure (no runtime changes).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +45 to +52
constructor(responseText: string, issues: OperationOutcomeIssue[], statusCode: number = 0) {
const message = issues.length > 0
? `\n * ${issues.map(i => i.toString()).join(" \n * ")}`
: responseText;
super(message);
this.name = this.constructor.name;
this.responseText = responseText;
this.issues = issues;
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrchestrateClientError's constructor now requires OperationOutcomeIssue[] for issues, which is a breaking TypeScript API change for any callers that previously constructed the error with a string[]. If the goal is “no breaking change for callers constructing the exception directly”, consider constructor overloads (or a union type) that accepts string[] and converts them internally.

Suggested change
constructor(responseText: string, issues: OperationOutcomeIssue[], statusCode: number = 0) {
const message = issues.length > 0
? `\n * ${issues.map(i => i.toString()).join(" \n * ")}`
: responseText;
super(message);
this.name = this.constructor.name;
this.responseText = responseText;
this.issues = issues;
constructor(responseText: string, issues: OperationOutcomeIssue[], statusCode?: number);
constructor(responseText: string, issues: string[], statusCode?: number);
constructor(responseText: string, issues: OperationOutcomeIssue[] | string[], statusCode: number = 0) {
const normalizedIssues = issues.map(issue =>
typeof issue === "string"
? new OperationOutcomeIssue("error", "unknown", issue, "")
: issue
);
const message = normalizedIssues.length > 0
? `\n * ${normalizedIssues.map(i => i.toString()).join(" \n * ")}`
: responseText;
super(message);
this.name = this.constructor.name;
this.responseText = responseText;
this.issues = normalizedIssues;

Copilot uses AI. Check for mistakes.
Comment on lines +28 to 32
async function readJsonOutcomes(responseText: string): Promise<OperationOutcomeIssue[]> {
try {
const json = JSON.parse(responseText);
if (json.issue) {
return json.issue.map((issue: any) => // eslint-disable-line @typescript-eslint/no-explicit-any
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the response body isn't a parseable OperationOutcome/problem+json, readJsonOutcomes returns [], which makes OrchestrateClientError.message fall back to raw responseText (no bullet formatting). Previously, the fallback path still produced a single “issue” string, so the message kept the bullet-list format; this contradicts the PR claim that the message format is unchanged.

Copilot uses AI. Check for mistakes.
Comment on lines 108 to 112
def _exception_from_response(response: requests.Response) -> OrchestrateHttpError:
operational_outcomes = _read_operational_outcomes(response)
issues = _read_json_outcomes(response)
if response.status_code >= 400 and response.status_code < 600:
return OrchestrateClientError(response.text, operational_outcomes)
return OrchestrateClientError(response.text, issues, response.status_code)
return OrchestrateHttpError()
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because _exception_from_response now passes an empty issues list through to OrchestrateClientError, non-JSON error responses will use the “Client error: …” message path instead of the prior “Issues: …” formatting. This is a user-visible behavior change; either preserve the prior formatting or adjust the PR description/tests accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to 67
def _read_json_outcomes(response: requests.Response) -> list[OperationOutcomeIssue]:
try:
json_response = response.json()
if "issue" in json_response:
return [
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _read_json_outcomes, non-JSON / non-OperationOutcome responses now result in an empty issues list (see function return behavior), whereas previously the fallback produced a single “issue” containing response.text. If keeping exception message formatting unchanged is important, consider restoring an equivalent fallback so callers still get a consistent “Issues:”/bullet-list message for non-structured errors.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@scottfavre scottfavre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just return the whole OperationOutcome? Trying to parse at this level will always result in data loss that may be important to clients.

severity: str
code: str
diagnostics: str
details: str
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code is just one of the values from https://build.fhir.org/valueset-issue-type.html

I think we need to know the detail.coding as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants