From 157f9fc4b45385620f005dba6c51db0043e3aa76 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 7 Jul 2026 15:34:35 +0000 Subject: [PATCH] =?UTF-8?q?Surface=20both=20canceljob=20failure=20shapes?= =?UTF-8?q?=20as=20verbatim=20rejections=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- infrastructure/abri/abri_client.py | 39 ++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index e6863386f..7c9c018a9 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -110,7 +110,7 @@ class AbriClient: if isinstance(outcome, AbriRequestRejected): return outcome - return self._parse_job_abandoned(outcome) + return self._parse_abandon_result(outcome) def get_tenant_data(self, place_ref: PlaceRef) -> GetTenantDataResult: outcome = self._exchange( @@ -210,20 +210,39 @@ class AbriClient: ) @staticmethod - def _parse_job_abandoned(root: ET.Element) -> JobAbandoned: + def _parse_abandon_result(root: ET.Element) -> AbandonJobResult: + # canceljob diverges from the other flows: failure can arrive as a + # result document whose cancellation collection is empty and which + # carries an ErrorDetails element, rather than the shared failure + # envelope. A present JobCancelled is success; a present ErrorDetails + # is a business rejection. Neither present is genuinely ambiguous and + # must stay retriable — never silently a success or a rejection. job_cancelled = root.find(".//JobCancelled") + if job_cancelled is not None: + job_no = job_cancelled.get("job_no") + if job_no is None: + raise AbriResponseParseError("JobCancelled element missing job_no") + return JobAbandoned(job_no=job_no) - if job_cancelled is None: + error_details = root.find(".//ErrorDetails") + if error_details is not None: + return AbriClient._rejection_from_error_details(error_details) + + raise AbriResponseParseError( + "canceljob response has neither JobCancelled nor ErrorDetails" + ) + + @staticmethod + def _rejection_from_error_details(error_details: ET.Element) -> AbriRequestRejected: + code = error_details.get("code") + message = error_details.get("message") + + if code is None or message is None: raise AbriResponseParseError( - "JobCancelled element missing from relay response" + "canceljob ErrorDetails missing code or message" ) - job_no = job_cancelled.get("job_no") - - if job_no is None: - raise AbriResponseParseError("JobCancelled element missing job_no") - - return JobAbandoned(job_no=job_no) + return AbriRequestRejected(code=code, message=message) @staticmethod def _parse_tenancy_data(root: ET.Element) -> TenancyData: