Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/reflex-base/news/5290.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve extra bound event arguments when `rx.upload_files` is used in an upload handler.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import env from "$/env.json";
* @param handler The handler to use.
* @param upload_id The upload id to use.
* @param on_upload_progress The function to call on upload progress.
* @param socket the websocket connection
* @param extra_headers Extra headers to send with the request.
* @param extra_args Extra bound handler args to forward to the backend handler.
* @param socket the websocket connection
* @param refs The refs object to store the abort controller in.
* @param getBackendURL Function to get the backend URL.
* @param getToken Function to get the Reflex token.
Expand All @@ -22,6 +23,7 @@ export const uploadFiles = async (
upload_id,
on_upload_progress,
extra_headers,
extra_args,
socket,
refs,
getBackendURL,
Expand Down Expand Up @@ -74,6 +76,13 @@ export const uploadFiles = async (
const controller = new AbortController();
const formdata = new FormData();

// Bound handler args are sent as a JSON form field that must precede the
// files, so the streaming chunk parser reads it before the first file part.
// Field name kept in sync with UPLOAD_EVENT_ARGS_FIELD in _upload.py.
if (extra_args && Object.keys(extra_args).length > 0) {
formdata.append("__reflex_event_args", JSON.stringify(extra_args));
}

// Add the token and handler to the file name.
files.forEach((file) => {
formdata.append("files", file, file.path || file.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,20 @@ export const applyEvent = async (event, socket, navigate, params) => {
*/
export const applyRestEvent = async (event, socket, navigate, params) => {
if (event.handler === "uploadFiles") {
// The compiled event names its extra bound handler args; collect just those
// so they reach the backend handler (no need to know the reserved keys).
const extra_args = {};
for (const name of event.payload.__reflex_event_arg_names ?? []) {
extra_args[name] = event.payload[name];
}
// Start upload, but do not wait for it, which would block other events.
uploadFiles(
event.name,
event.payload.files,
event.payload.upload_id,
event.payload.on_upload_progress,
event.payload.extra_headers,
extra_args,
socket,
refs,
getBackendURL,
Expand Down
59 changes: 50 additions & 9 deletions packages/reflex-base/src/reflex_base/event/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ def from_event_type(
EVENT_ACTIONS_MARKER = "_rx_event_actions"
UPLOAD_FILES_CLIENT_HANDLER = "uploadFiles"

# Payload key listing the names of the extra bound handler args in an upload
# event. Uploads use a REST endpoint instead of the socket; the args stay flat
# (so event-arg validation still sees them) and this manifest tells the client
# uploadFiles handler which ones to forward, without it hardcoding the reserved
# upload keys. Kept in sync with the matching literal in the web template.
UPLOAD_EVENT_ARG_NAMES_KEY = "__reflex_event_arg_names"


def _handler_name(handler: "EventHandler") -> str:
"""Get a stable fully qualified handler name for errors.
Expand Down Expand Up @@ -480,27 +487,61 @@ def __call__(self, *args: Any, **kwargs: Any) -> "EventSpec":
raise EventHandlerTypeError(msg)

fn_args = fn_args[: len(args)] + list(kwargs)

fn_args = (Var(_js_expr=arg) for arg in fn_args)
event_args = [*args, *kwargs.values()]

# Construct the payload.
values = []
for arg in [*args, *kwargs.values()]:
# Special case for file uploads.
payload = []
upload_event_spec = None
for fn_arg, arg in zip(fn_args, event_args, strict=False):
# Special case for file uploads. The upload arg takes its own
# positional slot so the remaining args stay aligned with fn_args,
# but its parameter name is re-derived server-side in as_event_spec.
if isinstance(arg, (FileUpload, UploadFilesChunk)):
return arg.as_event_spec(handler=self)
if upload_event_spec is not None:
msg = (
f"Event handler {self.fn.__name__} received multiple file "
"upload arguments."
)
raise EventHandlerTypeError(msg)
upload_event_spec = arg.as_event_spec(handler=self)
continue

# Otherwise, convert to JSON.
try:
values.append(LiteralVar.create(arg))
payload.append((Var(_js_expr=fn_arg), LiteralVar.create(arg)))
except TypeError as e:
msg = f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
raise EventHandlerTypeError(msg) from e
payload = tuple(zip(fn_args, values, strict=False))

if upload_event_spec is not None:
if not payload:
return upload_event_spec
# The extra bound args share the flat payload with the synthetic
# upload args, so reject names that would clobber a reserved upload
# key (files, upload_id, extra_headers, ...).
payload_names = [name._js_expr for name, _ in payload]
reserved = {name._js_expr for name, _ in upload_event_spec.args}
clash = next((name for name in payload_names if name in reserved), None)
if clash is not None:
msg = (
f"Event handler {self.fn.__name__} argument {clash!r} conflicts "
"with a reserved upload argument."
)
raise EventHandlerTypeError(msg)
# The client uploadFiles handler forwards exactly the args named here,
# so it never has to know the reserved upload keys.
return upload_event_spec.with_args((
*upload_event_spec.args,
*payload,
(
Var(_js_expr=UPLOAD_EVENT_ARG_NAMES_KEY),
LiteralVar.create(payload_names),
),
))

# Return the event spec.
return EventSpec(
handler=self, args=payload, event_actions=self.event_actions.copy()
handler=self, args=tuple(payload), event_actions=self.event_actions.copy()
)


Expand Down
1 change: 1 addition & 0 deletions packages/reflex-components-core/news/5290.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deliver extra bound handler arguments to upload handlers, so `on_drop=State.handle_upload(rx.upload_files(...), field)` passes `field` through to the backend instead of raising a missing-argument error.
2 changes: 1 addition & 1 deletion packages/reflex-components-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }]
maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }]
requires-python = ">=3.10"
dependencies = [
"reflex-base >= 0.9.5",
"reflex-base >= 0.9.5.post2.dev0",
"reflex-components-lucide >= 0.9.0",
"reflex-components-sonner >= 0.9.0",
"python_multipart >= 0.0.21",
Expand Down
Loading
Loading