Skip to content

Commit a17268c

Browse files
committed
fix(etl-uvicorn): map context errors through the blame taxonomy
An invocation_context with an unreadable schema_version is deployment skew between platform components; answering 422 let an upstream blame classifier pin it on the caller. Context failures now take their status from http_status_for like settings failures already did: malformed fields stay the caller's 422, version skew answers 500 with the class name only. Also documents the two capability tiers on /metadata: the unconditional strings are transport-level facts the middleware makes true for every wrapped app; invoke_with_sealed_dag_node_settings is the consumption claim and stays a per-plugin opt-in.
1 parent f0cd149 commit a17268c

2 files changed

Lines changed: 26 additions & 9 deletions

File tree

test/api/test_invocation_middleware.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,18 @@ def test_non_dict_reserved_field_is_rejected(self):
226226
assert sent[0]["status"] == 422
227227
assert b"invocation_settings" in sent[1]["body"]
228228

229-
def test_context_with_an_unreadable_schema_version_is_rejected(self):
229+
def test_context_with_an_unreadable_schema_version_fails_as_platform_error(self):
230230
# Absence means "older caller, use the boot settings"; a context this plugin cannot read
231-
# must not be downgraded to that.
231+
# must not be downgraded to that. And it is deployment skew, not a caller fault — a 422
232+
# would let an upstream blame classifier pin version skew on the customer.
232233
downstream, sent = _run_middleware(
233234
json.dumps({"invocation_context": {"schema_version": "99"}}).encode()
234235
)
235236

236237
assert downstream.body is None
237-
assert sent[0]["status"] == 422
238+
assert sent[0]["status"] == 500
238239
assert b"invocation_context" in sent[1]["body"]
240+
assert b"UnsupportedContextVersionError" in sent[1]["body"]
239241

240242
def test_malformed_context_is_rejected(self):
241243
downstream, sent = _run_middleware(

unstructured_platform_plugins/invocation_settings.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,15 @@ def add_metadata_route(
9292
`/metadata` is the plugin API spec's own discovery surface (`PluginMetadataOutput`): capability
9393
flags are strings in its `capabilities` list, which is where the controller looks before
9494
forwarding the reserved fields — no controller-private probe route.
95-
`invoke_with_sealed_dag_node_settings` additionally advertises that the plugin can be invoked
96-
with a sealed `dag_node_settings` member and open it itself.
95+
96+
The two tiers make different claims. `invocation_settings` / `invocation_context` are
97+
*transport-level* facts, advertised unconditionally because the installed middleware makes
98+
them true for every wrapped app: the reserved fields will be received, resolved, and bound —
99+
or the request failed. They say nothing about whether the handler reads the binding.
100+
`invoke_with_sealed_dag_node_settings` is the *consumption* claim — this plugin opens sealed
101+
`dag_node_settings` itself and its handler acts on the result — and stays a per-plugin opt-in
102+
set in the same change that makes it true, because it is the flag that invites the controller
103+
to seal settings to this pod in place of any other settings source.
97104
98105
Last call wins: the payload lives on `app.state` and every call overwrites it, while the route
99106
is registered once. A host wrapper may register with default capabilities at app construction
@@ -213,11 +220,19 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
213220
try:
214221
invocation_context = extract_context(parsed)
215222
except InvocationSettingsError as exc:
216-
# Includes an unknown schema_version: a producer this plugin cannot read fails
217-
# loudly here rather than running with silently absent identity. The message is
218-
# truncated because it can embed request-controlled values.
223+
# A context this plugin cannot read fails loudly here rather than running with
224+
# silently absent identity. Status comes from the blame taxonomy: a malformed
225+
# field is the caller's 422, but an unreadable schema_version is deployment skew
226+
# between platform components and must not read as a caller fault. The log line is
227+
# truncated because the message can embed request-controlled values.
219228
logger.warning("rejecting invalid %s: %.200s", RESERVED_CONTEXT_KEY, exc)
220-
await _send_json(send, 422, {"detail": f"Invalid field: {RESERVED_CONTEXT_KEY}"})
229+
status = http_status_for(exc)
230+
detail = (
231+
f"Invalid field: {RESERVED_CONTEXT_KEY}"
232+
if status == 422
233+
else f"Unusable {RESERVED_CONTEXT_KEY}: {type(exc).__name__}"
234+
)
235+
await _send_json(send, status, {"detail": detail})
221236
return
222237

223238
# The joined body and its parsed tree can be tens of MB and are not needed past this point;

0 commit comments

Comments
 (0)