Skip to content

Fix unsupported multipart data#102

Merged
touale merged 6 commits intomasterfrom
fix-unsupported-multipart-data
Mar 28, 2026
Merged

Fix unsupported multipart data#102
touale merged 6 commits intomasterfrom
fix-unsupported-multipart-data

Conversation

@touale
Copy link
Copy Markdown
Owner

@touale touale commented Mar 28, 2026

Summary by CodeRabbit

  • New Features

    • Added multipart/form-data file upload support for proxy endpoints and OpenAPI-driven proxy clients.
  • Refactor

    • Simplified plugin API resolution and removed resolver context management.
    • Updated route parameter handling to pass request kwargs directly.
  • Tests

    • Added/updated tests for proxy uploads, OpenAPI multipart schemas, plugin remote-API behavior, and auth token handling.
  • Chores

    • Added runtime dependency for multipart form-data handling.
    • Extended proxy endpoint whitelist in test configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Warning

Rate limit exceeded

@touale has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d9b0fae-3dbf-4eed-a93f-b66acbee8d68

📥 Commits

Reviewing files that changed from the base of the PR and between 3a04343 and 825a030.

📒 Files selected for processing (1)
  • src/framex/driver/ingress.py

Walkthrough

Removed the ApiResolver and resolver context machinery, centralized plugin API resolution on remote_apis, refactored ingress route handlers to forward request keyword args instead of building dynamic Pydantic models, and added multipart/form-data / file-upload support in the proxy plugin with OpenAPI-driven annotation resolution.

Changes

Cohort / File(s) Summary
Dependencies & Test Config
pyproject.toml, pytest.ini, tests/consts.py
Added python-multipart>=0.0.22, whitelisted /proxy/mock/upload in test env, and added OpenAPI MockUploadBody schema with binary file fields.
Plugin resolver removal & API call signatures
src/framex/plugin/resolver.py, src/framex/plugin/__init__.py, src/framex/plugin/base.py
Removed ApiResolver, its context-vars and helpers; added top-level coerce_plugin_api; dropped resolver param from _resolve_plugin_api/call_plugin_api; removed resolver-binding from BasePlugin.
Plugin load/on/model updates
src/framex/plugin/load.py, src/framex/plugin/on.py, src/framex/plugin/model.py
Call sites updated to remove resolver arg; PluginApi.tags narrowed to `list[str]
Ingress handler signature & request forwarding
src/framex/driver/ingress.py
Replaced dynamic Pydantic model creation with **request_kwargs capture; programmatically set route_handler.__signature__ to expose keyword-only params; updated register_route tags typing.
Proxy plugin multipart & OpenAPI parsing
src/framex/plugins/proxy/__init__.py, src/framex/plugins/proxy/builder.py
OpenAPI parser now recognizes multipart/form-data and binary/file schema markers; resolves properties to UploadFile or multipart Annotated types; dynamic method builds json/form/files and sends appropriate request kwargs.
Tests & mocks
tests/api/test_proxy.py, tests/mock.py, tests/adapter/test_ray_adapter.py, tests/driver/test_auth.py, tests/test_plugin.py, tests/consts.py
Added multipart upload tests and OpenAPI assertions; updated mock_request to normalize uploaded files; adjusted asyncio stub, JWT secret usage, and replaced resolver-based tests with remote_apis-based tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Ingress as Ingress<br/>(route handler)
    participant Proxy as Proxy<br/>Plugin
    participant OpenAPI as OpenAPI<br/>Parser
    participant Adapter as HTTP<br/>Adapter

    Client->>Ingress: POST /proxy/mock/upload (multipart: files + form)
    Ingress->>Proxy: forward **request_kwargs (params, data, files)
    Proxy->>OpenAPI: consult operation schema (requestBody)
    alt multipart/form-data
        OpenAPI-->>Proxy: field schemas (mark file vs form)
        Proxy->>Proxy: build form_body + files tuples
    else application/json
        OpenAPI-->>Proxy: single $ref schema
        Proxy->>Proxy: build json_body (Pydantic model)
    end
    Proxy->>Adapter: fetch_response(request_kwargs: data/files/json, params)
    Adapter-->>Client: HTTP response (proxied result)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop, I nibbled the resolver away,

Files now tumble in multipart play,
Kwargs whisper where models once stood,
OpenAPI guides the upload good,
Hooray — carrots and bytes all day!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix unsupported multipart data' directly addresses the main change: adding comprehensive multipart/form-data support throughout the codebase, including python-multipart dependency, OpenAPI schema updates, and file upload handling in proxy operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-unsupported-multipart-data

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 28, 2026

Codecov Report

❌ Patch coverage is 81.63265% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/framex/plugins/proxy/__init__.py 83.67% 4 Missing and 4 partials ⚠️
src/framex/plugins/proxy/builder.py 66.66% 4 Missing and 3 partials ⚠️
src/framex/plugin/resolver.py 71.42% 1 Missing and 1 partial ⚠️
src/framex/plugin/on.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/adapter/test_ray_adapter.py (1)

183-191: Good fix to properly dispose of the coroutine.

Calling coro.close() prevents "coroutine was never awaited" warnings. The test correctly verifies the wrapper calls asyncio.run and returns its result.

Minor cleanup: The call_log list (line 162) and the logging inside async_func are now unused since the coroutine is closed without execution. Consider removing the dead code:

🧹 Proposed cleanup
-        call_log = []
-
         async def async_func(x):
-            call_log.append(f"called with {x}")
             return x * 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/adapter/test_ray_adapter.py` around lines 183 - 191, Remove the
now-unused test artifacts: delete the call_log list and any logging inside
async_func since the coroutine is closed by _consume_coroutine and never
executed; update references in the test around _consume_coroutine and
captured_wrapper to ensure no remaining uses of call_log or async_func remain,
and run the test to confirm behavior is unchanged (mock_asyncio_run still
asserts called and result from captured_wrapper remains 10).
src/framex/plugins/proxy/__init__.py (2)

240-241: Remove or uncomment the decorator.

Commented-out code suggests incomplete work. If @logger.catch is intentionally disabled, consider removing it entirely or adding a comment explaining why it's disabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugins/proxy/__init__.py` around lines 240 - 241, The
commented-out decorator `# `@logger.catch`` above the async function
`fetch_response` should be resolved: either remove the commented line entirely
if it's unnecessary, or uncomment it to re-enable structured error logging; if
leaving it disabled is intentional, add a short explanatory comment (e.g., why
logging was disabled or conditions under which it should be re-enabled) directly
above `fetch_response` so future readers know the rationale.

152-156: Unreachable code branch.

This else block can never execute because content_type is set to either "application/json" or "multipart/form-data" by lines 122-130. Unsupported content types already trigger continue at line 130.

Proposed fix - remove dead code
                         if is_upload_annotation(annotation):
                             file_param_names.add(field_name)
-                    else:
-                        logger.opt(colors=True).error(
-                            f"Failed to proxy api({method}) <r>{url}{path}</r>, unsupported content type: ${body_content.keys()}"
-                        )
-                        continue
                 logger.opt(colors=True).trace(f"Found proxy api({method}) <g>{url}{path}</g>")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugins/proxy/__init__.py` around lines 152 - 156, The else branch
that logs "unsupported content type" using logger.opt(...).error and references
body_content is unreachable because content_type is already gated to
"application/json" or "multipart/form-data" earlier; remove that dead else block
(the logger.opt(...).error and continue) from the proxy handling loop in
__init__.py and ensure no remaining references to body_content or that
unreachable path remain; if you intended to handle other content types instead,
replace the removal by adding an explicit fallback branch that inspects
content_type and logs the actual unsupported type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framex/driver/ingress.py`:
- Around line 31-33: Delete the unused helper function _is_basemodel_annotation
(which calls _unwrap_annotation and checks issubclass against BaseModel) from
the file; remove its entire definition to avoid dead code and unused
imports/refs, and ensure no other code references _is_basemodel_annotation
before committing.

In `@src/framex/plugins/proxy/__init__.py`:
- Around line 127-130: The f-string in the logger call uses a stray dollar sign
before {body_content.keys()}, which is invalid Python f-string syntax; in the
proxy logging statement (logger.opt(...).error(...)) remove the `$` so the
expression is interpolated as {body_content.keys()} (i.e., change
"${body_content.keys()}" to "{body_content.keys()}"), keeping the rest of the
message and variables (method, url, path, body_content) intact.

In `@tests/mock.py`:
- Around line 49-62: The files comprehension in the mock response builds file
dicts by unpacking (field_name, file_info) but iterates over files directly,
which yields keys and breaks unpacking; in the elif branch that handles
url.endswith("/proxy/mock/upload") and method == "POST" (inside tests/mock.py)
change the comprehension to iterate over (files or {}).items() so it correctly
gets (field_name, file_info) pairs and handles None safely.

---

Nitpick comments:
In `@src/framex/plugins/proxy/__init__.py`:
- Around line 240-241: The commented-out decorator `# `@logger.catch`` above the
async function `fetch_response` should be resolved: either remove the commented
line entirely if it's unnecessary, or uncomment it to re-enable structured error
logging; if leaving it disabled is intentional, add a short explanatory comment
(e.g., why logging was disabled or conditions under which it should be
re-enabled) directly above `fetch_response` so future readers know the
rationale.
- Around line 152-156: The else branch that logs "unsupported content type"
using logger.opt(...).error and references body_content is unreachable because
content_type is already gated to "application/json" or "multipart/form-data"
earlier; remove that dead else block (the logger.opt(...).error and continue)
from the proxy handling loop in __init__.py and ensure no remaining references
to body_content or that unreachable path remain; if you intended to handle other
content types instead, replace the removal by adding an explicit fallback branch
that inspects content_type and logs the actual unsupported type.

In `@tests/adapter/test_ray_adapter.py`:
- Around line 183-191: Remove the now-unused test artifacts: delete the call_log
list and any logging inside async_func since the coroutine is closed by
_consume_coroutine and never executed; update references in the test around
_consume_coroutine and captured_wrapper to ensure no remaining uses of call_log
or async_func remain, and run the test to confirm behavior is unchanged
(mock_asyncio_run still asserts called and result from captured_wrapper remains
10).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 867f0ed5-90b5-4183-93ae-c219ff64cdf3

📥 Commits

Reviewing files that changed from the base of the PR and between 486c17e and 84f3f60.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • pyproject.toml
  • pytest.ini
  • src/framex/driver/ingress.py
  • src/framex/plugin/__init__.py
  • src/framex/plugin/base.py
  • src/framex/plugin/load.py
  • src/framex/plugin/model.py
  • src/framex/plugin/on.py
  • src/framex/plugin/resolver.py
  • src/framex/plugins/proxy/__init__.py
  • src/framex/plugins/proxy/builder.py
  • tests/adapter/test_ray_adapter.py
  • tests/api/test_proxy.py
  • tests/consts.py
  • tests/driver/test_auth.py
  • tests/mock.py
  • tests/test_plugin.py
💤 Files with no reviewable changes (1)
  • src/framex/plugin/load.py

Comment thread src/framex/driver/ingress.py Outdated
Comment thread src/framex/plugins/proxy/__init__.py
Comment thread tests/mock.py
Comment on lines +49 to +62
elif url.endswith("/proxy/mock/upload") and method == "POST":
resp.json.return_value = {
"method": "POST",
"params": params,
"body": body,
"files": [
{
"field": field_name,
"filename": file_info[0],
"content_type": file_info[2],
}
for field_name, file_info in (files or [])
],
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Run the specific test to verify if it passes with current implementation
cd tests && python -c "
files = {'ppt_file': ('demo.pptx', b'content', 'application/pptx'), 'txt_file': ('demo.txt', b'txt', 'text/plain')}
try:
    result = [(field_name, file_info) for field_name, file_info in files]
    print('Direct iteration result:', result)
except Exception as e:
    print('Error:', e)
    
result_items = [(field_name, file_info) for field_name, file_info in files.items()]
print('Using .items():', result_items)
"

Repository: touale/FrameX-kit

Length of output: 234


🏁 Script executed:

cat -n tests/mock.py | sed -n '45,65p'

Repository: touale/FrameX-kit

Length of output: 917


🏁 Script executed:

cat -n tests/test_proxy.py | sed -n '30,60p'

Repository: touale/FrameX-kit

Length of output: 112


🏁 Script executed:

find . -name "*.py" -path "*/tests/*" -type f | head -20

Repository: touale/FrameX-kit

Length of output: 600


🏁 Script executed:

rg "proxy/mock/upload" --type py

Repository: touale/FrameX-kit

Length of output: 381


🏁 Script executed:

cat -n tests/api/test_proxy.py | sed -n '1,100p'

Repository: touale/FrameX-kit

Length of output: 4171


🏁 Script executed:

cd tests && python -m pytest api/test_proxy.py::test_get_proxy_upload -v

Repository: touale/FrameX-kit

Length of output: 245


Fix dict iteration in files comprehension.

When files is passed as a dict (as shown in the test: files={"ppt_file": (...), "txt_file": (...)}), iterating directly over it yields only keys, not (key, value) pairs. The unpacking for field_name, file_info in (files or []) fails with "too many values to unpack". Use .items() instead.

🐛 Proposed fix
     elif url.endswith("/proxy/mock/upload") and method == "POST":
         resp.json.return_value = {
             "method": "POST",
             "params": params,
             "body": body,
             "files": [
                 {
                     "field": field_name,
                     "filename": file_info[0],
                     "content_type": file_info[2],
                 }
-                for field_name, file_info in (files or [])
+                for field_name, file_info in (files or {}).items()
             ],
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/mock.py` around lines 49 - 62, The files comprehension in the mock
response builds file dicts by unpacking (field_name, file_info) but iterates
over files directly, which yields keys and breaks unpacking; in the elif branch
that handles url.endswith("/proxy/mock/upload") and method == "POST" (inside
tests/mock.py) change the comprehension to iterate over (files or {}).items() so
it correctly gets (field_name, file_info) pairs and handles None safely.

@touale touale merged commit 37fb293 into master Mar 28, 2026
8 of 9 checks passed
@touale touale deleted the fix-unsupported-multipart-data branch March 28, 2026 09:00
@coderabbitai coderabbitai bot mentioned this pull request Apr 9, 2026
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