From 57d34f0e4886078ffeca99777d465600c7a86a29 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 8 Sep 2025 19:51:19 -0700 Subject: [PATCH 1/9] add headers to upload js --- reflex/.templates/web/utils/helpers/upload.js | 160 ++++++++++++++++++ reflex/.templates/web/utils/state.js | 159 +---------------- 2 files changed, 161 insertions(+), 158 deletions(-) create mode 100644 reflex/.templates/web/utils/helpers/upload.js diff --git a/reflex/.templates/web/utils/helpers/upload.js b/reflex/.templates/web/utils/helpers/upload.js new file mode 100644 index 00000000000..883ac57d413 --- /dev/null +++ b/reflex/.templates/web/utils/helpers/upload.js @@ -0,0 +1,160 @@ +import JSON5 from "json5"; +import env from "$/env.json"; +import { refs, getBackendURL, getToken } from "./state"; + +/** + * Upload files to the server. + * + * @param state The state to apply the delta to. + * @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 + * + * @returns The response from posting to the UPLOADURL endpoint. + */ +export const uploadFiles = async ( + handler, + files, + upload_id, + on_upload_progress, + socket, +) => { + // return if there's no file to upload + if (files === undefined || files.length === 0) { + return false; + } + + const upload_ref_name = `__upload_controllers_${upload_id}`; + + if (refs[upload_ref_name]) { + console.log("Upload already in progress for ", upload_id); + return false; + } + + // Track how many partial updates have been processed for this upload. + let resp_idx = 0; + const eventHandler = (progressEvent) => { + const event_callbacks = socket._callbacks.$event; + // Whenever called, responseText will contain the entire response so far. + const chunks = progressEvent.event.target.responseText.trim().split("\n"); + // So only process _new_ chunks beyond resp_idx. + chunks.slice(resp_idx).map((chunk_json) => { + try { + const chunk = JSON5.parse(chunk_json); + event_callbacks.map((f, ix) => { + f(chunk) + .then(() => { + if (ix === event_callbacks.length - 1) { + // Mark this chunk as processed. + resp_idx += 1; + } + }) + .catch((e) => { + if (progressEvent.progress === 1) { + // Chunk may be incomplete, so only report errors when full response is available. + console.log("Error processing chunk", chunk, e); + } + return; + }); + }); + } catch (e) { + if (progressEvent.progress === 1) { + console.log("Error parsing chunk", chunk_json, e); + } + return; + } + }); + }; + + const controller = new AbortController(); + const formdata = new FormData(); + + // Add the token and handler to the file name. + files.forEach((file) => { + formdata.append("files", file, file.path || file.name); + }); + + // Send the file to the server. + refs[upload_ref_name] = controller; + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + // Set up event handlers + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 300) { + resolve({ + data: xhr.responseText, + status: xhr.status, + statusText: xhr.statusText, + headers: { + get: (name) => xhr.getResponseHeader(name), + }, + }); + } else { + reject(new Error(`HTTP error! status: ${xhr.status}`)); + } + }; + + xhr.onerror = function () { + reject(new Error("Network error")); + }; + + xhr.onabort = function () { + reject(new Error("Upload aborted")); + }; + + // Handle upload progress + if (on_upload_progress) { + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + const progressEvent = { + loaded: event.loaded, + total: event.total, + progress: event.loaded / event.total, + }; + on_upload_progress(progressEvent); + } + }; + } + + // Handle download progress with streaming response parsing + xhr.onprogress = function (event) { + if (eventHandler) { + const progressEvent = { + event: { + target: { + responseText: xhr.responseText, + }, + }, + progress: event.lengthComputable ? event.loaded / event.total : 0, + }; + eventHandler(progressEvent); + } + }; + + // Handle abort controller + controller.signal.addEventListener("abort", () => { + xhr.abort(); + }); + + // Configure and send request + xhr.open("POST", getBackendURL(env.UPLOAD)); + xhr.setRequestHeader("Reflex-Client-Token", getToken()); + xhr.setRequestHeader("Reflex-Event-Handler", handler); + + try { + xhr.send(formdata); + } catch (error) { + reject(error); + } + }) + .catch((error) => { + console.log("Upload error:", error.message); + return false; + }) + .finally(() => { + delete refs[upload_ref_name]; + }); +}; \ No newline at end of file diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 01eaa4dd915..cd0f57d58a1 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -20,10 +20,10 @@ import { } from "$/utils/context"; import debounce from "$/utils/helpers/debounce"; import throttle from "$/utils/helpers/throttle"; +import { uploadFiles } from "$/utils/helpers/upload"; // Endpoint URLs. const EVENTURL = env.EVENT; -const UPLOADURL = env.UPLOAD; // These hostnames indicate that the backend and frontend are reachable via the same domain. const SAME_DOMAIN_HOSTNAMES = ["localhost", "0.0.0.0", "::", "0:0:0:0:0:0:0:0"]; @@ -614,163 +614,6 @@ export const connect = async ( document.addEventListener("visibilitychange", checkVisibility); }; -/** - * Upload files to the server. - * - * @param state The state to apply the delta to. - * @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 - * - * @returns The response from posting to the UPLOADURL endpoint. - */ -export const uploadFiles = async ( - handler, - files, - upload_id, - on_upload_progress, - socket, -) => { - // return if there's no file to upload - if (files === undefined || files.length === 0) { - return false; - } - - const upload_ref_name = `__upload_controllers_${upload_id}`; - - if (refs[upload_ref_name]) { - console.log("Upload already in progress for ", upload_id); - return false; - } - - // Track how many partial updates have been processed for this upload. - let resp_idx = 0; - const eventHandler = (progressEvent) => { - const event_callbacks = socket._callbacks.$event; - // Whenever called, responseText will contain the entire response so far. - const chunks = progressEvent.event.target.responseText.trim().split("\n"); - // So only process _new_ chunks beyond resp_idx. - chunks.slice(resp_idx).map((chunk_json) => { - try { - const chunk = JSON5.parse(chunk_json); - event_callbacks.map((f, ix) => { - f(chunk) - .then(() => { - if (ix === event_callbacks.length - 1) { - // Mark this chunk as processed. - resp_idx += 1; - } - }) - .catch((e) => { - if (progressEvent.progress === 1) { - // Chunk may be incomplete, so only report errors when full response is available. - console.log("Error processing chunk", chunk, e); - } - return; - }); - }); - } catch (e) { - if (progressEvent.progress === 1) { - console.log("Error parsing chunk", chunk_json, e); - } - return; - } - }); - }; - - const controller = new AbortController(); - const formdata = new FormData(); - - // Add the token and handler to the file name. - files.forEach((file) => { - formdata.append("files", file, file.path || file.name); - }); - - // Send the file to the server. - refs[upload_ref_name] = controller; - - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - // Set up event handlers - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 300) { - resolve({ - data: xhr.responseText, - status: xhr.status, - statusText: xhr.statusText, - headers: { - get: (name) => xhr.getResponseHeader(name), - }, - }); - } else { - reject(new Error(`HTTP error! status: ${xhr.status}`)); - } - }; - - xhr.onerror = function () { - reject(new Error("Network error")); - }; - - xhr.onabort = function () { - reject(new Error("Upload aborted")); - }; - - // Handle upload progress - if (on_upload_progress) { - xhr.upload.onprogress = function (event) { - if (event.lengthComputable) { - const progressEvent = { - loaded: event.loaded, - total: event.total, - progress: event.loaded / event.total, - }; - on_upload_progress(progressEvent); - } - }; - } - - // Handle download progress with streaming response parsing - xhr.onprogress = function (event) { - if (eventHandler) { - const progressEvent = { - event: { - target: { - responseText: xhr.responseText, - }, - }, - progress: event.lengthComputable ? event.loaded / event.total : 0, - }; - eventHandler(progressEvent); - } - }; - - // Handle abort controller - controller.signal.addEventListener("abort", () => { - xhr.abort(); - }); - - // Configure and send request - xhr.open("POST", getBackendURL(UPLOADURL)); - xhr.setRequestHeader("Reflex-Client-Token", getToken()); - xhr.setRequestHeader("Reflex-Event-Handler", handler); - - try { - xhr.send(formdata); - } catch (error) { - reject(error); - } - }) - .catch((error) => { - console.log("Upload error:", error.message); - return false; - }) - .finally(() => { - delete refs[upload_ref_name]; - }); -}; - /** * Create an event object. * @param {string} name The name of the event. From 131512c44ba48723585d3376805a8c1b8e3ac265 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 8 Sep 2025 20:20:41 -0700 Subject: [PATCH 2/9] pass it from environment --- reflex/.templates/web/utils/helpers/upload.js | 160 --------------- reflex/.templates/web/utils/state.js | 3 + reflex/app.py | 5 + reflex/compiler/compiler.py | 26 +++ reflex/compiler/templates.py | 184 ++++++++++++++++++ reflex/compiler/utils.py | 9 + reflex/constants/base.py | 2 + reflex/environment.py | 23 ++- 8 files changed, 251 insertions(+), 161 deletions(-) delete mode 100644 reflex/.templates/web/utils/helpers/upload.js diff --git a/reflex/.templates/web/utils/helpers/upload.js b/reflex/.templates/web/utils/helpers/upload.js deleted file mode 100644 index 883ac57d413..00000000000 --- a/reflex/.templates/web/utils/helpers/upload.js +++ /dev/null @@ -1,160 +0,0 @@ -import JSON5 from "json5"; -import env from "$/env.json"; -import { refs, getBackendURL, getToken } from "./state"; - -/** - * Upload files to the server. - * - * @param state The state to apply the delta to. - * @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 - * - * @returns The response from posting to the UPLOADURL endpoint. - */ -export const uploadFiles = async ( - handler, - files, - upload_id, - on_upload_progress, - socket, -) => { - // return if there's no file to upload - if (files === undefined || files.length === 0) { - return false; - } - - const upload_ref_name = `__upload_controllers_${upload_id}`; - - if (refs[upload_ref_name]) { - console.log("Upload already in progress for ", upload_id); - return false; - } - - // Track how many partial updates have been processed for this upload. - let resp_idx = 0; - const eventHandler = (progressEvent) => { - const event_callbacks = socket._callbacks.$event; - // Whenever called, responseText will contain the entire response so far. - const chunks = progressEvent.event.target.responseText.trim().split("\n"); - // So only process _new_ chunks beyond resp_idx. - chunks.slice(resp_idx).map((chunk_json) => { - try { - const chunk = JSON5.parse(chunk_json); - event_callbacks.map((f, ix) => { - f(chunk) - .then(() => { - if (ix === event_callbacks.length - 1) { - // Mark this chunk as processed. - resp_idx += 1; - } - }) - .catch((e) => { - if (progressEvent.progress === 1) { - // Chunk may be incomplete, so only report errors when full response is available. - console.log("Error processing chunk", chunk, e); - } - return; - }); - }); - } catch (e) { - if (progressEvent.progress === 1) { - console.log("Error parsing chunk", chunk_json, e); - } - return; - } - }); - }; - - const controller = new AbortController(); - const formdata = new FormData(); - - // Add the token and handler to the file name. - files.forEach((file) => { - formdata.append("files", file, file.path || file.name); - }); - - // Send the file to the server. - refs[upload_ref_name] = controller; - - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - // Set up event handlers - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 300) { - resolve({ - data: xhr.responseText, - status: xhr.status, - statusText: xhr.statusText, - headers: { - get: (name) => xhr.getResponseHeader(name), - }, - }); - } else { - reject(new Error(`HTTP error! status: ${xhr.status}`)); - } - }; - - xhr.onerror = function () { - reject(new Error("Network error")); - }; - - xhr.onabort = function () { - reject(new Error("Upload aborted")); - }; - - // Handle upload progress - if (on_upload_progress) { - xhr.upload.onprogress = function (event) { - if (event.lengthComputable) { - const progressEvent = { - loaded: event.loaded, - total: event.total, - progress: event.loaded / event.total, - }; - on_upload_progress(progressEvent); - } - }; - } - - // Handle download progress with streaming response parsing - xhr.onprogress = function (event) { - if (eventHandler) { - const progressEvent = { - event: { - target: { - responseText: xhr.responseText, - }, - }, - progress: event.lengthComputable ? event.loaded / event.total : 0, - }; - eventHandler(progressEvent); - } - }; - - // Handle abort controller - controller.signal.addEventListener("abort", () => { - xhr.abort(); - }); - - // Configure and send request - xhr.open("POST", getBackendURL(env.UPLOAD)); - xhr.setRequestHeader("Reflex-Client-Token", getToken()); - xhr.setRequestHeader("Reflex-Event-Handler", handler); - - try { - xhr.send(formdata); - } catch (error) { - reject(error); - } - }) - .catch((error) => { - console.log("Upload error:", error.message); - return false; - }) - .finally(() => { - delete refs[upload_ref_name]; - }); -}; \ No newline at end of file diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index cd0f57d58a1..ffc1cbde4cd 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -433,6 +433,9 @@ export const applyRestEvent = async (event, socket, navigate, params) => { event.payload.upload_id, event.payload.on_upload_progress, socket, + refs, + getBackendURL, + getToken, ); return false; } diff --git a/reflex/app.py b/reflex/app.py index a298d1beef0..1fd68d3db72 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1422,6 +1422,11 @@ def _submit_work_without_advancing( compile_results.append( compiler.compile_contexts(self._state, self.theme), ) + compile_results.append( + compiler.compile_upload_js( + environment.REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS.get().items() + ) + ) if self.theme is not None: # Fix #2992 by removing the top-level appearance prop self.theme.appearance = None # pyright: ignore[reportAttributeAccessIssue] diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 7e78b36bf85..e6ee6f0c181 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -145,6 +145,18 @@ def _compile_contexts(state: type[BaseState] | None, theme: Component | None) -> ) +def _compile_upload_js(extra_headers: Iterable[tuple[str, str]]) -> str: + """Compile the upload.js file. + + Args: + extra_headers: Extra headers to include in the upload request. + + Returns: + The compiled upload.js file. + """ + return templates.upload_js_template(extra_headers=extra_headers) + + def _compile_page(component: BaseComponent) -> str: """Compile the component. @@ -547,6 +559,20 @@ def compile_contexts( return output_path, _compile_contexts(state, theme) +def compile_upload_js(extra_headers: Iterable[tuple[str, str]]) -> tuple[str, str]: + """Compile the upload.js file. + + Args: + extra_headers: Extra headers to include in the upload request. + + Returns: + The path and code of the compiled upload.js file. + """ + output_path = utils.get_upload_js_path() + + return output_path, _compile_upload_js(extra_headers=extra_headers) + + def compile_page(path: str, component: BaseComponent) -> tuple[str, str]: """Compile a single page. diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 2cccdb41eb4..51111666ad2 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -709,3 +709,187 @@ def _render_hooks(hooks: dict[str, VarData | None], memo: list | None = None) -> post_trigger_str = "\n".join(post_trigger) memo_str = "\n".join(memo) if memo is not None else "" return f"{internal_str}\n{pre_trigger_str}\n{memo_str}\n{post_trigger_str}" + + +def upload_js_template(extra_headers: Iterable[tuple[str, str]]) -> str: + """Template for upload.js. + + Args: + extra_headers: Extra headers to include in the upload request. + + Returns: + Rendered upload.js content as string. + """ + add_extra_headers = "\n".join( + [ + f" xhr.setRequestHeader({json.dumps(name)}, {json.dumps(value)});" + for name, value in extra_headers + ] + ) + return rf"""import JSON5 from "json5"; +import env from "$/env.json"; + +/** + * Upload files to the server. + * + * @param state The state to apply the delta to. + * @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 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. + * + * @returns The response from posting to the UPLOADURL endpoint. + */ +export const uploadFiles = async ( + handler, + files, + upload_id, + on_upload_progress, + socket, + refs, + getBackendURL, + getToken, +) => {{ + // return if there's no file to upload + if (files === undefined || files.length === 0) {{ + return false; + }} + + const upload_ref_name = `__upload_controllers_${{upload_id}}`; + + if (refs[upload_ref_name]) {{ + console.log("Upload already in progress for ", upload_id); + return false; + }} + + // Track how many partial updates have been processed for this upload. + let resp_idx = 0; + const eventHandler = (progressEvent) => {{ + const event_callbacks = socket._callbacks.$event; + // Whenever called, responseText will contain the entire response so far. + const chunks = progressEvent.event.target.responseText.trim().split("\n"); + // So only process _new_ chunks beyond resp_idx. + chunks.slice(resp_idx).map((chunk_json) => {{ + try {{ + const chunk = JSON5.parse(chunk_json); + event_callbacks.map((f, ix) => {{ + f(chunk) + .then(() => {{ + if (ix === event_callbacks.length - 1) {{ + // Mark this chunk as processed. + resp_idx += 1; + }} + }}) + .catch((e) => {{ + if (progressEvent.progress === 1) {{ + // Chunk may be incomplete, so only report errors when full response is available. + console.log("Error processing chunk", chunk, e); + }} + return; + }}); + }}); + }} catch (e) {{ + if (progressEvent.progress === 1) {{ + console.log("Error parsing chunk", chunk_json, e); + }} + return; + }} + }}); + }}; + + const controller = new AbortController(); + const formdata = new FormData(); + + // Add the token and handler to the file name. + files.forEach((file) => {{ + formdata.append("files", file, file.path || file.name); + }}); + + // Send the file to the server. + refs[upload_ref_name] = controller; + + return new Promise((resolve, reject) => {{ + const xhr = new XMLHttpRequest(); + + // Set up event handlers + xhr.onload = function () {{ + if (xhr.status >= 200 && xhr.status < 300) {{ + resolve({{ + data: xhr.responseText, + status: xhr.status, + statusText: xhr.statusText, + headers: {{ + get: (name) => xhr.getResponseHeader(name), + }}, + }}); + }} else {{ + reject(new Error(`HTTP error! status: ${{xhr.status}}`)); + }} + }}; + + xhr.onerror = function () {{ + reject(new Error("Network error")); + }}; + + xhr.onabort = function () {{ + reject(new Error("Upload aborted")); + }}; + + // Handle upload progress + if (on_upload_progress) {{ + xhr.upload.onprogress = function (event) {{ + if (event.lengthComputable) {{ + const progressEvent = {{ + loaded: event.loaded, + total: event.total, + progress: event.loaded / event.total, + }}; + on_upload_progress(progressEvent); + }} + }}; + }} + + // Handle download progress with streaming response parsing + xhr.onprogress = function (event) {{ + if (eventHandler) {{ + const progressEvent = {{ + event: {{ + target: {{ + responseText: xhr.responseText, + }}, + }}, + progress: event.lengthComputable ? event.loaded / event.total : 0, + }}; + eventHandler(progressEvent); + }} + }}; + + // Handle abort controller + controller.signal.addEventListener("abort", () => {{ + xhr.abort(); + }}); + + // Configure and send request + xhr.open("POST", getBackendURL(env.UPLOAD)); + xhr.setRequestHeader("Reflex-Client-Token", getToken()); + xhr.setRequestHeader("Reflex-Event-Handler", handler); +{add_extra_headers} + + try {{ + xhr.send(formdata); + }} catch (error) {{ + reject(error); + }} + }}) + .catch((error) => {{ + console.log("Upload error:", error.message); + return false; + }}) + .finally(() => {{ + delete refs[upload_ref_name]; + }}); +}}; +""" diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 9a748ca1f7e..0750a76ebbb 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -526,6 +526,15 @@ def get_context_path() -> str: return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS)) +def get_upload_js_path() -> str: + """Get the path of the upload.js file. + + Returns: + The path of the upload.js file. + """ + return str(get_web_dir() / (constants.Dirs.UPLOAD_PATH + constants.Ext.JS)) + + def get_components_path() -> str: """Get the path of the compiled components. diff --git a/reflex/constants/base.py b/reflex/constants/base.py index e33eb30352f..b1b03518efd 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -36,6 +36,8 @@ class Dirs(SimpleNamespace): COMPONENTS_PATH = UTILS + "/components" # The name of the contexts file. CONTEXTS_PATH = UTILS + "/context" + # The name of the helpers file. + UPLOAD_PATH = UTILS + "/helpers/upload" # The name of the output directory. BUILD_DIR = "build" # The name of the static files directory. diff --git a/reflex/environment.py b/reflex/environment.py index 9cef0565cae..bedb613075a 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -9,7 +9,7 @@ import multiprocessing import os import platform -from collections.abc import Callable, Sequence +from collections.abc import Callable, Mapping, Sequence from functools import lru_cache from pathlib import Path from typing import ( @@ -226,6 +226,24 @@ def interpret_env_var_value( return interpret_existing_path_env(value, field_name) if field_type is Plugin: return interpret_plugin_env(value, field_name) + if get_origin(field_type) is Mapping: + args = get_args(field_type) + if len(args) != 2: + msg = f"Invalid mapping type for environment variable {field_name}: {field_type}. Must have exactly two type arguments." + raise ValueError(msg) + items = value.split(":") + if len(items) % 2 != 0: + msg = f"Invalid mapping value: {value!r} for {field_name}. Must be in the format key1:value1:key2:value2" + raise EnvironmentVarValueError(msg) + keys = [ + interpret_env_var_value(k, args[0], f"{field_name}[{i}].key") + for i, k in enumerate(items[0::2]) + ] + vals = [ + interpret_env_var_value(v, args[1], f"{field_name}[{i}].value") + for i, v in enumerate(items[1::2]) + ] + return dict(zip(keys, vals, strict=True)) if get_origin(field_type) in (list, Sequence): return [ interpret_env_var_value( @@ -657,6 +675,9 @@ class EnvironmentVariables: # Whether to force a full reload on changes. VITE_FORCE_FULL_RELOAD: EnvVar[bool] = env_var(False) + # Extra headers to include in the upload request to the REFLEX_UPLOAD_ENDPOINT. Formatted as key1:value1:key2:value2 + REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS: EnvVar[Mapping[str, str]] = env_var({}) + environment = EnvironmentVariables() From 3ffa9bb434c3f87fbb9535c46908f8040ffbfb9a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 8 Sep 2025 20:29:28 -0700 Subject: [PATCH 3/9] fix precommit --- reflex/environment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reflex/environment.py b/reflex/environment.py index bedb613075a..eab970bdb8f 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -204,7 +204,8 @@ def interpret_env_var_value( The interpreted value. Raises: - ValueError: If the value is invalid. + ValueError: If the environment variable type is invalid. + EnvironmentVarValueError: If the environment variable value is invalid. """ field_type = value_inside_optional(field_type) From 76e461f6f9230647844fefee88ad9231e46774df Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Sep 2025 11:01:22 -0700 Subject: [PATCH 4/9] make it a runtime thing --- reflex/.templates/web/utils/helpers/upload.js | 170 ++++++++++++++++ reflex/.templates/web/utils/state.js | 1 + reflex/app.py | 5 - reflex/compiler/compiler.py | 26 --- reflex/compiler/templates.py | 184 ------------------ reflex/event.py | 7 + uploaded_files/test.txt | 1 + 7 files changed, 179 insertions(+), 215 deletions(-) create mode 100644 reflex/.templates/web/utils/helpers/upload.js create mode 100644 uploaded_files/test.txt diff --git a/reflex/.templates/web/utils/helpers/upload.js b/reflex/.templates/web/utils/helpers/upload.js new file mode 100644 index 00000000000..5b84f32b0ff --- /dev/null +++ b/reflex/.templates/web/utils/helpers/upload.js @@ -0,0 +1,170 @@ +import JSON5 from "json5"; +import env from "$/env.json"; + +/** + * Upload files to the server. + * + * @param state The state to apply the delta to. + * @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 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. + * + * @returns The response from posting to the UPLOADURL endpoint. + */ +export const uploadFiles = async ( + handler, + files, + upload_id, + on_upload_progress, + extra_headers, + socket, + refs, + getBackendURL, + getToken +) => { + // return if there's no file to upload + if (files === undefined || files.length === 0) { + return false; + } + + const upload_ref_name = `__upload_controllers_${upload_id}`; + + if (refs[upload_ref_name]) { + console.log("Upload already in progress for ", upload_id); + return false; + } + + // Track how many partial updates have been processed for this upload. + let resp_idx = 0; + const eventHandler = (progressEvent) => { + const event_callbacks = socket._callbacks.$event; + // Whenever called, responseText will contain the entire response so far. + const chunks = progressEvent.event.target.responseText.trim().split("\n"); + // So only process _new_ chunks beyond resp_idx. + chunks.slice(resp_idx).map((chunk_json) => { + try { + const chunk = JSON5.parse(chunk_json); + event_callbacks.map((f, ix) => { + f(chunk) + .then(() => { + if (ix === event_callbacks.length - 1) { + // Mark this chunk as processed. + resp_idx += 1; + } + }) + .catch((e) => { + if (progressEvent.progress === 1) { + // Chunk may be incomplete, so only report errors when full response is available. + console.log("Error processing chunk", chunk, e); + } + return; + }); + }); + } catch (e) { + if (progressEvent.progress === 1) { + console.log("Error parsing chunk", chunk_json, e); + } + return; + } + }); + }; + + const controller = new AbortController(); + const formdata = new FormData(); + + // Add the token and handler to the file name. + files.forEach((file) => { + formdata.append("files", file, file.path || file.name); + }); + + // Send the file to the server. + refs[upload_ref_name] = controller; + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + // Set up event handlers + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 300) { + resolve({ + data: xhr.responseText, + status: xhr.status, + statusText: xhr.statusText, + headers: { + get: (name) => xhr.getResponseHeader(name), + }, + }); + } else { + reject(new Error(`HTTP error! status: ${xhr.status}`)); + } + }; + + xhr.onerror = function () { + reject(new Error("Network error")); + }; + + xhr.onabort = function () { + reject(new Error("Upload aborted")); + }; + + // Handle upload progress + if (on_upload_progress) { + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + const progressEvent = { + loaded: event.loaded, + total: event.total, + progress: event.loaded / event.total, + }; + on_upload_progress(progressEvent); + } + }; + } + + // Handle download progress with streaming response parsing + xhr.onprogress = function (event) { + if (eventHandler) { + const progressEvent = { + event: { + target: { + responseText: xhr.responseText, + }, + }, + progress: event.lengthComputable ? event.loaded / event.total : 0, + }; + eventHandler(progressEvent); + } + }; + + // Handle abort controller + controller.signal.addEventListener("abort", () => { + xhr.abort(); + }); + + // Configure and send request + xhr.open("POST", getBackendURL(env.UPLOAD)); + xhr.setRequestHeader("Reflex-Client-Token", getToken()); + xhr.setRequestHeader("Reflex-Event-Handler", handler); + for (const [key, value] of Object.entries(extra_headers || {})) { + xhr.setRequestHeader(key, value); + } + + try { + xhr.send(formdata); + } catch (error) { + reject(error); + } + }) + .catch((error) => { + console.log("Upload error:", error.message); + return false; + }) + .finally(() => { + delete refs[upload_ref_name]; + }); +}; diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index ffc1cbde4cd..a63fa4f09c8 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -432,6 +432,7 @@ export const applyRestEvent = async (event, socket, navigate, params) => { event.payload.files, event.payload.upload_id, event.payload.on_upload_progress, + event.payload.extra_headers, socket, refs, getBackendURL, diff --git a/reflex/app.py b/reflex/app.py index 1fd68d3db72..a298d1beef0 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1422,11 +1422,6 @@ def _submit_work_without_advancing( compile_results.append( compiler.compile_contexts(self._state, self.theme), ) - compile_results.append( - compiler.compile_upload_js( - environment.REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS.get().items() - ) - ) if self.theme is not None: # Fix #2992 by removing the top-level appearance prop self.theme.appearance = None # pyright: ignore[reportAttributeAccessIssue] diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index e6ee6f0c181..7e78b36bf85 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -145,18 +145,6 @@ def _compile_contexts(state: type[BaseState] | None, theme: Component | None) -> ) -def _compile_upload_js(extra_headers: Iterable[tuple[str, str]]) -> str: - """Compile the upload.js file. - - Args: - extra_headers: Extra headers to include in the upload request. - - Returns: - The compiled upload.js file. - """ - return templates.upload_js_template(extra_headers=extra_headers) - - def _compile_page(component: BaseComponent) -> str: """Compile the component. @@ -559,20 +547,6 @@ def compile_contexts( return output_path, _compile_contexts(state, theme) -def compile_upload_js(extra_headers: Iterable[tuple[str, str]]) -> tuple[str, str]: - """Compile the upload.js file. - - Args: - extra_headers: Extra headers to include in the upload request. - - Returns: - The path and code of the compiled upload.js file. - """ - output_path = utils.get_upload_js_path() - - return output_path, _compile_upload_js(extra_headers=extra_headers) - - def compile_page(path: str, component: BaseComponent) -> tuple[str, str]: """Compile a single page. diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 51111666ad2..2cccdb41eb4 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -709,187 +709,3 @@ def _render_hooks(hooks: dict[str, VarData | None], memo: list | None = None) -> post_trigger_str = "\n".join(post_trigger) memo_str = "\n".join(memo) if memo is not None else "" return f"{internal_str}\n{pre_trigger_str}\n{memo_str}\n{post_trigger_str}" - - -def upload_js_template(extra_headers: Iterable[tuple[str, str]]) -> str: - """Template for upload.js. - - Args: - extra_headers: Extra headers to include in the upload request. - - Returns: - Rendered upload.js content as string. - """ - add_extra_headers = "\n".join( - [ - f" xhr.setRequestHeader({json.dumps(name)}, {json.dumps(value)});" - for name, value in extra_headers - ] - ) - return rf"""import JSON5 from "json5"; -import env from "$/env.json"; - -/** - * Upload files to the server. - * - * @param state The state to apply the delta to. - * @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 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. - * - * @returns The response from posting to the UPLOADURL endpoint. - */ -export const uploadFiles = async ( - handler, - files, - upload_id, - on_upload_progress, - socket, - refs, - getBackendURL, - getToken, -) => {{ - // return if there's no file to upload - if (files === undefined || files.length === 0) {{ - return false; - }} - - const upload_ref_name = `__upload_controllers_${{upload_id}}`; - - if (refs[upload_ref_name]) {{ - console.log("Upload already in progress for ", upload_id); - return false; - }} - - // Track how many partial updates have been processed for this upload. - let resp_idx = 0; - const eventHandler = (progressEvent) => {{ - const event_callbacks = socket._callbacks.$event; - // Whenever called, responseText will contain the entire response so far. - const chunks = progressEvent.event.target.responseText.trim().split("\n"); - // So only process _new_ chunks beyond resp_idx. - chunks.slice(resp_idx).map((chunk_json) => {{ - try {{ - const chunk = JSON5.parse(chunk_json); - event_callbacks.map((f, ix) => {{ - f(chunk) - .then(() => {{ - if (ix === event_callbacks.length - 1) {{ - // Mark this chunk as processed. - resp_idx += 1; - }} - }}) - .catch((e) => {{ - if (progressEvent.progress === 1) {{ - // Chunk may be incomplete, so only report errors when full response is available. - console.log("Error processing chunk", chunk, e); - }} - return; - }}); - }}); - }} catch (e) {{ - if (progressEvent.progress === 1) {{ - console.log("Error parsing chunk", chunk_json, e); - }} - return; - }} - }}); - }}; - - const controller = new AbortController(); - const formdata = new FormData(); - - // Add the token and handler to the file name. - files.forEach((file) => {{ - formdata.append("files", file, file.path || file.name); - }}); - - // Send the file to the server. - refs[upload_ref_name] = controller; - - return new Promise((resolve, reject) => {{ - const xhr = new XMLHttpRequest(); - - // Set up event handlers - xhr.onload = function () {{ - if (xhr.status >= 200 && xhr.status < 300) {{ - resolve({{ - data: xhr.responseText, - status: xhr.status, - statusText: xhr.statusText, - headers: {{ - get: (name) => xhr.getResponseHeader(name), - }}, - }}); - }} else {{ - reject(new Error(`HTTP error! status: ${{xhr.status}}`)); - }} - }}; - - xhr.onerror = function () {{ - reject(new Error("Network error")); - }}; - - xhr.onabort = function () {{ - reject(new Error("Upload aborted")); - }}; - - // Handle upload progress - if (on_upload_progress) {{ - xhr.upload.onprogress = function (event) {{ - if (event.lengthComputable) {{ - const progressEvent = {{ - loaded: event.loaded, - total: event.total, - progress: event.loaded / event.total, - }}; - on_upload_progress(progressEvent); - }} - }}; - }} - - // Handle download progress with streaming response parsing - xhr.onprogress = function (event) {{ - if (eventHandler) {{ - const progressEvent = {{ - event: {{ - target: {{ - responseText: xhr.responseText, - }}, - }}, - progress: event.lengthComputable ? event.loaded / event.total : 0, - }}; - eventHandler(progressEvent); - }} - }}; - - // Handle abort controller - controller.signal.addEventListener("abort", () => {{ - xhr.abort(); - }}); - - // Configure and send request - xhr.open("POST", getBackendURL(env.UPLOAD)); - xhr.setRequestHeader("Reflex-Client-Token", getToken()); - xhr.setRequestHeader("Reflex-Event-Handler", handler); -{add_extra_headers} - - try {{ - xhr.send(formdata); - }} catch (error) {{ - reject(error); - }} - }}) - .catch((error) => {{ - console.log("Upload error:", error.message); - return false; - }}) - .finally(() => {{ - delete refs[upload_ref_name]; - }}); -}}; -""" diff --git a/reflex/event.py b/reflex/event.py index ff4b84e9616..99d2a27f461 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -872,6 +872,7 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: DEFAULT_UPLOAD_ID, upload_files_context_var_data, ) + from reflex.environment import environment upload_id = self.upload_id or DEFAULT_UPLOAD_ID spec_args = [ @@ -887,6 +888,12 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: Var(_js_expr="upload_id"), LiteralVar.create(upload_id), ), + ( + Var(_js_expr="extra_headers"), + LiteralVar.create( + dict(environment.REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS.get()) + ), + ), ] if self.on_upload_progress is not None: on_upload_progress = self.on_upload_progress diff --git a/uploaded_files/test.txt b/uploaded_files/test.txt new file mode 100644 index 00000000000..fdffb5316f1 --- /dev/null +++ b/uploaded_files/test.txt @@ -0,0 +1 @@ +test file contents! \ No newline at end of file From e3aa2bd9cbc4070768126d7eab45c14ec435e975 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Sep 2025 11:03:41 -0700 Subject: [PATCH 5/9] remove dead code --- reflex/.templates/web/utils/helpers/upload.js | 2 +- reflex/compiler/utils.py | 9 --------- reflex/constants/base.py | 2 -- uploaded_files/test.txt | 1 - 4 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 uploaded_files/test.txt diff --git a/reflex/.templates/web/utils/helpers/upload.js b/reflex/.templates/web/utils/helpers/upload.js index 5b84f32b0ff..6bbfc746ed6 100644 --- a/reflex/.templates/web/utils/helpers/upload.js +++ b/reflex/.templates/web/utils/helpers/upload.js @@ -25,7 +25,7 @@ export const uploadFiles = async ( socket, refs, getBackendURL, - getToken + getToken, ) => { // return if there's no file to upload if (files === undefined || files.length === 0) { diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 0750a76ebbb..9a748ca1f7e 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -526,15 +526,6 @@ def get_context_path() -> str: return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS)) -def get_upload_js_path() -> str: - """Get the path of the upload.js file. - - Returns: - The path of the upload.js file. - """ - return str(get_web_dir() / (constants.Dirs.UPLOAD_PATH + constants.Ext.JS)) - - def get_components_path() -> str: """Get the path of the compiled components. diff --git a/reflex/constants/base.py b/reflex/constants/base.py index b1b03518efd..e33eb30352f 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -36,8 +36,6 @@ class Dirs(SimpleNamespace): COMPONENTS_PATH = UTILS + "/components" # The name of the contexts file. CONTEXTS_PATH = UTILS + "/context" - # The name of the helpers file. - UPLOAD_PATH = UTILS + "/helpers/upload" # The name of the output directory. BUILD_DIR = "build" # The name of the static files directory. diff --git a/uploaded_files/test.txt b/uploaded_files/test.txt deleted file mode 100644 index fdffb5316f1..00000000000 --- a/uploaded_files/test.txt +++ /dev/null @@ -1 +0,0 @@ -test file contents! \ No newline at end of file From 868eef8170e74add1c1ee1278970329a54904013 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Sep 2025 11:08:45 -0700 Subject: [PATCH 6/9] add tests for environment mapping --- tests/units/test_environment.py | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/units/test_environment.py b/tests/units/test_environment.py index cc8f9bd5df5..c370f2c6aa4 100644 --- a/tests/units/test_environment.py +++ b/tests/units/test_environment.py @@ -196,6 +196,87 @@ def test_interpret_optional_type(self): result = interpret_env_var_value("42", int | None, "TEST_FIELD") assert result == 42 + def test_interpret_mapping_string_int(self): + """Test mapping interpretation with str:int types.""" + from collections.abc import Mapping + + result = interpret_env_var_value( + "key1:1:key2:2", Mapping[str, int], "TEST_FIELD" + ) + assert result == {"key1": 1, "key2": 2} + + def test_interpret_mapping_int_string(self): + """Test mapping interpretation with int:str types.""" + from collections.abc import Mapping + + result = interpret_env_var_value( + "1:value1:2:value2", Mapping[int, str], "TEST_FIELD" + ) + assert result == {1: "value1", 2: "value2"} + + def test_interpret_mapping_empty(self): + """Test mapping interpretation with empty value.""" + from collections.abc import Mapping + + result = interpret_env_var_value("", Mapping[str, str], "TEST_FIELD") + assert result == {} + + def test_interpret_mapping_single_pair(self): + """Test mapping interpretation with single key-value pair.""" + from collections.abc import Mapping + + result = interpret_env_var_value("key:value", Mapping[str, str], "TEST_FIELD") + assert result == {"key": "value"} + + def test_interpret_mapping_bool_values(self): + """Test mapping interpretation with boolean values.""" + from collections.abc import Mapping + + result = interpret_env_var_value( + "key1:true:key2:false", Mapping[str, bool], "TEST_FIELD" + ) + assert result == {"key1": True, "key2": False} + + def test_interpret_mapping_odd_length_error(self): + """Test mapping interpretation with odd number of items raises error.""" + from collections.abc import Mapping + + with pytest.raises( + EnvironmentVarValueError, + match="Invalid mapping value.*Must be in the format key1:value1:key2:value2", + ): + interpret_env_var_value("key1:value1:key2", Mapping[str, str], "TEST_FIELD") + + def test_interpret_mapping_invalid_type_args(self): + """Test mapping interpretation with invalid number of type arguments.""" + from collections.abc import Mapping + + with pytest.raises( + ValueError, + match="Invalid mapping type.*Must have exactly two type arguments", + ): + interpret_env_var_value("key:value", Mapping[str], "TEST_FIELD") # pyright: ignore[reportInvalidTypeArguments] + + def test_interpret_mapping_nested_types(self): + """Test mapping interpretation with path types.""" + from collections.abc import Mapping + from pathlib import Path + + result = interpret_env_var_value( + "key1:/path/one:key2:/path/two", Mapping[str, Path], "TEST_FIELD" + ) + assert result == {"key1": Path("/path/one"), "key2": Path("/path/two")} + assert all(isinstance(v, Path) for v in result.values()) + + def test_interpret_mapping_enum_keys(self): + """Test mapping interpretation with enum keys.""" + from collections.abc import Mapping + + result = interpret_env_var_value( + "value1:test1:value2:test2", Mapping[_TestEnum, str], "TEST_FIELD" + ) + assert result == {_TestEnum.VALUE1: "test1", _TestEnum.VALUE2: "test2"} + class TestEnvVar: """Test the EnvVar class.""" From c94cfd066e301f9d8d2fe46d44ab63a861fc8bb3 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Sep 2025 11:10:21 -0700 Subject: [PATCH 7/9] fix tests --- reflex/environment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reflex/environment.py b/reflex/environment.py index eab970bdb8f..bc6421ff4c4 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -232,7 +232,9 @@ def interpret_env_var_value( if len(args) != 2: msg = f"Invalid mapping type for environment variable {field_name}: {field_type}. Must have exactly two type arguments." raise ValueError(msg) - items = value.split(":") + if not value.strip(): + return {} + items = value.strip().split(":") if len(items) % 2 != 0: msg = f"Invalid mapping value: {value!r} for {field_name}. Must be in the format key1:value1:key2:value2" raise EnvironmentVarValueError(msg) From 60fadf5ef3a2565e103a95116df6ab83df78a381 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Sep 2025 13:49:09 -0700 Subject: [PATCH 8/9] do things without env vars --- reflex/environment.py | 26 +---------- reflex/event.py | 6 +-- tests/units/test_environment.py | 81 --------------------------------- 3 files changed, 3 insertions(+), 110 deletions(-) diff --git a/reflex/environment.py b/reflex/environment.py index bc6421ff4c4..a9d2384a489 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -9,7 +9,7 @@ import multiprocessing import os import platform -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Sequence from functools import lru_cache from pathlib import Path from typing import ( @@ -205,7 +205,6 @@ def interpret_env_var_value( Raises: ValueError: If the environment variable type is invalid. - EnvironmentVarValueError: If the environment variable value is invalid. """ field_type = value_inside_optional(field_type) @@ -227,26 +226,6 @@ def interpret_env_var_value( return interpret_existing_path_env(value, field_name) if field_type is Plugin: return interpret_plugin_env(value, field_name) - if get_origin(field_type) is Mapping: - args = get_args(field_type) - if len(args) != 2: - msg = f"Invalid mapping type for environment variable {field_name}: {field_type}. Must have exactly two type arguments." - raise ValueError(msg) - if not value.strip(): - return {} - items = value.strip().split(":") - if len(items) % 2 != 0: - msg = f"Invalid mapping value: {value!r} for {field_name}. Must be in the format key1:value1:key2:value2" - raise EnvironmentVarValueError(msg) - keys = [ - interpret_env_var_value(k, args[0], f"{field_name}[{i}].key") - for i, k in enumerate(items[0::2]) - ] - vals = [ - interpret_env_var_value(v, args[1], f"{field_name}[{i}].value") - for i, v in enumerate(items[1::2]) - ] - return dict(zip(keys, vals, strict=True)) if get_origin(field_type) in (list, Sequence): return [ interpret_env_var_value( @@ -678,9 +657,6 @@ class EnvironmentVariables: # Whether to force a full reload on changes. VITE_FORCE_FULL_RELOAD: EnvVar[bool] = env_var(False) - # Extra headers to include in the upload request to the REFLEX_UPLOAD_ENDPOINT. Formatted as key1:value1:key2:value2 - REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS: EnvVar[Mapping[str, str]] = env_var({}) - environment = EnvironmentVariables() diff --git a/reflex/event.py b/reflex/event.py index 99d2a27f461..2cb63bbba2a 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -846,6 +846,7 @@ class FileUpload: upload_id: str | None = None on_upload_progress: EventHandler | Callable | None = None + extra_headers: dict[str, str] | None = None @staticmethod def on_upload_progress_args_spec(_prog: Var[dict[str, int | float | bool]]): @@ -872,7 +873,6 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: DEFAULT_UPLOAD_ID, upload_files_context_var_data, ) - from reflex.environment import environment upload_id = self.upload_id or DEFAULT_UPLOAD_ID spec_args = [ @@ -890,9 +890,7 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: ), ( Var(_js_expr="extra_headers"), - LiteralVar.create( - dict(environment.REFLEX_UPLOAD_ENDPOINT_EXTRA_HEADERS.get()) - ), + LiteralVar.create(self.extra_headers or {}), ), ] if self.on_upload_progress is not None: diff --git a/tests/units/test_environment.py b/tests/units/test_environment.py index c370f2c6aa4..cc8f9bd5df5 100644 --- a/tests/units/test_environment.py +++ b/tests/units/test_environment.py @@ -196,87 +196,6 @@ def test_interpret_optional_type(self): result = interpret_env_var_value("42", int | None, "TEST_FIELD") assert result == 42 - def test_interpret_mapping_string_int(self): - """Test mapping interpretation with str:int types.""" - from collections.abc import Mapping - - result = interpret_env_var_value( - "key1:1:key2:2", Mapping[str, int], "TEST_FIELD" - ) - assert result == {"key1": 1, "key2": 2} - - def test_interpret_mapping_int_string(self): - """Test mapping interpretation with int:str types.""" - from collections.abc import Mapping - - result = interpret_env_var_value( - "1:value1:2:value2", Mapping[int, str], "TEST_FIELD" - ) - assert result == {1: "value1", 2: "value2"} - - def test_interpret_mapping_empty(self): - """Test mapping interpretation with empty value.""" - from collections.abc import Mapping - - result = interpret_env_var_value("", Mapping[str, str], "TEST_FIELD") - assert result == {} - - def test_interpret_mapping_single_pair(self): - """Test mapping interpretation with single key-value pair.""" - from collections.abc import Mapping - - result = interpret_env_var_value("key:value", Mapping[str, str], "TEST_FIELD") - assert result == {"key": "value"} - - def test_interpret_mapping_bool_values(self): - """Test mapping interpretation with boolean values.""" - from collections.abc import Mapping - - result = interpret_env_var_value( - "key1:true:key2:false", Mapping[str, bool], "TEST_FIELD" - ) - assert result == {"key1": True, "key2": False} - - def test_interpret_mapping_odd_length_error(self): - """Test mapping interpretation with odd number of items raises error.""" - from collections.abc import Mapping - - with pytest.raises( - EnvironmentVarValueError, - match="Invalid mapping value.*Must be in the format key1:value1:key2:value2", - ): - interpret_env_var_value("key1:value1:key2", Mapping[str, str], "TEST_FIELD") - - def test_interpret_mapping_invalid_type_args(self): - """Test mapping interpretation with invalid number of type arguments.""" - from collections.abc import Mapping - - with pytest.raises( - ValueError, - match="Invalid mapping type.*Must have exactly two type arguments", - ): - interpret_env_var_value("key:value", Mapping[str], "TEST_FIELD") # pyright: ignore[reportInvalidTypeArguments] - - def test_interpret_mapping_nested_types(self): - """Test mapping interpretation with path types.""" - from collections.abc import Mapping - from pathlib import Path - - result = interpret_env_var_value( - "key1:/path/one:key2:/path/two", Mapping[str, Path], "TEST_FIELD" - ) - assert result == {"key1": Path("/path/one"), "key2": Path("/path/two")} - assert all(isinstance(v, Path) for v in result.values()) - - def test_interpret_mapping_enum_keys(self): - """Test mapping interpretation with enum keys.""" - from collections.abc import Mapping - - result = interpret_env_var_value( - "value1:test1:value2:test2", Mapping[_TestEnum, str], "TEST_FIELD" - ) - assert result == {_TestEnum.VALUE1: "test1", _TestEnum.VALUE2: "test2"} - class TestEnvVar: """Test the EnvVar class.""" From ca680db5770c6ee6e2b73297e7ef5ae976290c5f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Sep 2025 14:35:51 -0700 Subject: [PATCH 9/9] is not None --- reflex/event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index 2cb63bbba2a..8cce59e4e78 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -890,7 +890,9 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: ), ( Var(_js_expr="extra_headers"), - LiteralVar.create(self.extra_headers or {}), + LiteralVar.create( + self.extra_headers if self.extra_headers is not None else {} + ), ), ] if self.on_upload_progress is not None: