Skip to content

refactor(etl-uvicorn): bind /invoke envelope without body replay - #75

Merged
CyMule merged 2 commits into
feat/etl-uvicorn-invocation-settingsfrom
feat/invoke-envelope-dependency
Aug 10, 2026
Merged

refactor(etl-uvicorn): bind /invoke envelope without body replay#75
CyMule merged 2 commits into
feat/etl-uvicorn-invocation-settingsfrom
feat/invoke-envelope-dependency

Conversation

@CyMule

@CyMule CyMule commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace raw /invoke body buffering and replay with a FastAPI dependency that reads Starlette's cached JSON parse.
  • Keep InvokeBodyLimitMiddleware below FastAPI as a streaming byte counter, so oversized bodies are rejected without a second buffer.
  • Install binding through the router's public dependency list before POST /invoke is registered; remove private route.dependant mutation.
  • Re-enter the captured invocation binding inside async-generator response iteration, so streaming plugins see settings on the repository's locked FastAPI 0.117.1 as well as newer FastAPI releases.
  • Preserve the explicit invoke_with_sealed_dag_node_settings capability opt-in and its wrapper, generator, and CLI arguments.
  • Align transport tests with the shared contract: sealed settings are accepted only at invocation_settings.dag_node_settings; a bare envelope fails closed.

Why

ASGI middleware has to consume the raw receive channel before FastAPI can parse it, which forced the old implementation to buffer, parse, and replay the request. Route-level extraction shares the framework's body and JSON caches instead.

The initial dependency version still had three correctness problems: FastAPI 0.117.1 closed yield dependencies before StreamingResponse iteration, installation mutated FastAPI's private dependency graph after route registration, and unconditional sealed-capability advertisement conflated transport support with handler consumption. This revision fixes all three without raising the FastAPI floor.

Impact

  • install_invocation_envelope(app) must run before POST /invoke is registered. It may run after unrelated routes such as /metadata, which preserves the hand-written plugin integration order.
  • The dependency is a path/method-aware no-op outside POST /invoke, including mixed-method routes and rooted deployments.
  • Malformed JSON on a declared FastAPI body model continues to use FastAPI's own validation response.

Validation

  • pytest -q on FastAPI 0.117.1 / Starlette 0.48.0 with the current utic-invocation-settings 0.4.0 branch — 153 passed.
  • Focused transport tests on FastAPI 0.141.1 / Starlette 1.6.0 — 38 passed.
  • ruff check . — clean.
  • ruff format --check on changed Python files — clean.
  • git diff --check — clean.

…endency, drop the sealed-settings opt-in

The body-replay middleware is replaced by bind_invocation_envelope, a FastAPI
dependency that reads the framework's Starlette-cached body parse, so the
/invoke body is buffered and decoded exactly once — no receive replay, no
second copy of a large body. The request-size cap stays below the framework as
InvokeBodyLimitMiddleware, a streaming byte counter that never buffers.

/metadata now advertises invoke_with_sealed_dag_node_settings unconditionally:
sealed per-invoke settings are the platform's required settings path, so the
wrap_in_fastapi/generate_fast_api parameter and --sealed-dag-node-settings CLI
flag are removed.
@CyMule
CyMule force-pushed the feat/invoke-envelope-dependency branch from 136431f to 30b0112 Compare August 5, 2026 19:49

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="unstructured_platform_plugins/invocation_settings.py">

<violation number="1" location="unstructured_platform_plugins/invocation_settings.py:248">
P1: Streaming `/invoke` plugins lose the reserved invocation bindings on the repository's locked FastAPI 0.117.1. This `yield` dependency resets `_INVOCATION` when the path operation returns, but `wrap_in_fastapi` only starts iterating an async-generator plugin after that point while sending its `StreamingResponse`; the plugin therefore sees `current_invocation_settings()` and `current_invocation_context()` as `None`. Keeping the context around the stream (and adding a streaming binding test), or requiring FastAPI >= 0.118.0 and updating the lock, would preserve sealed settings and request identity for streaming plugins.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

) from exc

with invocation_envelope(invocation_settings, invocation_context):
yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Streaming /invoke plugins lose the reserved invocation bindings on the repository's locked FastAPI 0.117.1. This yield dependency resets _INVOCATION when the path operation returns, but wrap_in_fastapi only starts iterating an async-generator plugin after that point while sending its StreamingResponse; the plugin therefore sees current_invocation_settings() and current_invocation_context() as None. Keeping the context around the stream (and adding a streaming binding test), or requiring FastAPI >= 0.118.0 and updating the lock, would preserve sealed settings and request identity for streaming plugins.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unstructured_platform_plugins/invocation_settings.py, line 248:

<comment>Streaming `/invoke` plugins lose the reserved invocation bindings on the repository's locked FastAPI 0.117.1. This `yield` dependency resets `_INVOCATION` when the path operation returns, but `wrap_in_fastapi` only starts iterating an async-generator plugin after that point while sending its `StreamingResponse`; the plugin therefore sees `current_invocation_settings()` and `current_invocation_context()` as `None`. Keeping the context around the stream (and adding a streaming binding test), or requiring FastAPI >= 0.118.0 and updating the lock, would preserve sealed settings and request identity for streaming plugins.</comment>

<file context>
@@ -147,6 +156,98 @@ async def plugin_metadata() -> dict:
+            ) from exc
+
+    with invocation_envelope(invocation_settings, invocation_context):
+        yield
+
+
</file context>

Comment thread unstructured_platform_plugins/invocation_settings.py Outdated
@CyMule CyMule changed the title refactor(etl-uvicorn): bind reserved /invoke fields via a route dependency; no sealed-settings opt-out refactor(etl-uvicorn): bind /invoke envelope without body replay Aug 10, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="unstructured_platform_plugins/invocation_settings.py">

<violation number="1" location="unstructured_platform_plugins/invocation_settings.py:135">
P1: Default `wrap_in_fastapi` plugins no longer advertise sealed-settings support, so the controller will not send per-invoke sealed settings and handlers can fall back to boot-time state. Always include `invoke_with_sealed_dag_node_settings` as required by this PR's no-opt-out contract.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

Comment on lines 135 to 136
capabilities = [RESERVED_ENVELOPE_KEY, RESERVED_CONTEXT_KEY]
if invoke_with_sealed_dag_node_settings:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Default wrap_in_fastapi plugins no longer advertise sealed-settings support, so the controller will not send per-invoke sealed settings and handlers can fall back to boot-time state. Always include invoke_with_sealed_dag_node_settings as required by this PR's no-opt-out contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unstructured_platform_plugins/invocation_settings.py, line 135:

<comment>Default `wrap_in_fastapi` plugins no longer advertise sealed-settings support, so the controller will not send per-invoke sealed settings and handlers can fall back to boot-time state. Always include `invoke_with_sealed_dag_node_settings` as required by this PR's no-opt-out contract.</comment>

<file context>
@@ -114,33 +112,33 @@ def invocation_envelope(
     is registered once. A host wrapper may register at app construction and a plugin can still
     re-register with its own identifier afterwards, with no route-order dependence.
     """
+    capabilities = [RESERVED_ENVELOPE_KEY, RESERVED_CONTEXT_KEY]
+    if invoke_with_sealed_dag_node_settings:
+        capabilities.append(INVOKE_WITH_SEALED_DAG_NODE_SETTINGS_CAPABILITY)
</file context>

@CyMule
CyMule merged commit 8908808 into feat/etl-uvicorn-invocation-settings Aug 10, 2026
3 checks passed
@CyMule
CyMule deleted the feat/invoke-envelope-dependency branch August 10, 2026 18:10
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.

1 participant