Skip to content

Commit a605e7a

Browse files
authored
add extra headers to upload event spec (#5771)
* add headers to upload js * pass it from environment * fix precommit * make it a runtime thing * remove dead code * add tests for environment mapping * fix tests * do things without env vars * is not None
1 parent 2e0e20f commit a605e7a

File tree

4 files changed

+183
-159
lines changed

4 files changed

+183
-159
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import JSON5 from "json5";
2+
import env from "$/env.json";
3+
4+
/**
5+
* Upload files to the server.
6+
*
7+
* @param state The state to apply the delta to.
8+
* @param handler The handler to use.
9+
* @param upload_id The upload id to use.
10+
* @param on_upload_progress The function to call on upload progress.
11+
* @param socket the websocket connection
12+
* @param extra_headers Extra headers to send with the request.
13+
* @param refs The refs object to store the abort controller in.
14+
* @param getBackendURL Function to get the backend URL.
15+
* @param getToken Function to get the Reflex token.
16+
*
17+
* @returns The response from posting to the UPLOADURL endpoint.
18+
*/
19+
export const uploadFiles = async (
20+
handler,
21+
files,
22+
upload_id,
23+
on_upload_progress,
24+
extra_headers,
25+
socket,
26+
refs,
27+
getBackendURL,
28+
getToken,
29+
) => {
30+
// return if there's no file to upload
31+
if (files === undefined || files.length === 0) {
32+
return false;
33+
}
34+
35+
const upload_ref_name = `__upload_controllers_${upload_id}`;
36+
37+
if (refs[upload_ref_name]) {
38+
console.log("Upload already in progress for ", upload_id);
39+
return false;
40+
}
41+
42+
// Track how many partial updates have been processed for this upload.
43+
let resp_idx = 0;
44+
const eventHandler = (progressEvent) => {
45+
const event_callbacks = socket._callbacks.$event;
46+
// Whenever called, responseText will contain the entire response so far.
47+
const chunks = progressEvent.event.target.responseText.trim().split("\n");
48+
// So only process _new_ chunks beyond resp_idx.
49+
chunks.slice(resp_idx).map((chunk_json) => {
50+
try {
51+
const chunk = JSON5.parse(chunk_json);
52+
event_callbacks.map((f, ix) => {
53+
f(chunk)
54+
.then(() => {
55+
if (ix === event_callbacks.length - 1) {
56+
// Mark this chunk as processed.
57+
resp_idx += 1;
58+
}
59+
})
60+
.catch((e) => {
61+
if (progressEvent.progress === 1) {
62+
// Chunk may be incomplete, so only report errors when full response is available.
63+
console.log("Error processing chunk", chunk, e);
64+
}
65+
return;
66+
});
67+
});
68+
} catch (e) {
69+
if (progressEvent.progress === 1) {
70+
console.log("Error parsing chunk", chunk_json, e);
71+
}
72+
return;
73+
}
74+
});
75+
};
76+
77+
const controller = new AbortController();
78+
const formdata = new FormData();
79+
80+
// Add the token and handler to the file name.
81+
files.forEach((file) => {
82+
formdata.append("files", file, file.path || file.name);
83+
});
84+
85+
// Send the file to the server.
86+
refs[upload_ref_name] = controller;
87+
88+
return new Promise((resolve, reject) => {
89+
const xhr = new XMLHttpRequest();
90+
91+
// Set up event handlers
92+
xhr.onload = function () {
93+
if (xhr.status >= 200 && xhr.status < 300) {
94+
resolve({
95+
data: xhr.responseText,
96+
status: xhr.status,
97+
statusText: xhr.statusText,
98+
headers: {
99+
get: (name) => xhr.getResponseHeader(name),
100+
},
101+
});
102+
} else {
103+
reject(new Error(`HTTP error! status: ${xhr.status}`));
104+
}
105+
};
106+
107+
xhr.onerror = function () {
108+
reject(new Error("Network error"));
109+
};
110+
111+
xhr.onabort = function () {
112+
reject(new Error("Upload aborted"));
113+
};
114+
115+
// Handle upload progress
116+
if (on_upload_progress) {
117+
xhr.upload.onprogress = function (event) {
118+
if (event.lengthComputable) {
119+
const progressEvent = {
120+
loaded: event.loaded,
121+
total: event.total,
122+
progress: event.loaded / event.total,
123+
};
124+
on_upload_progress(progressEvent);
125+
}
126+
};
127+
}
128+
129+
// Handle download progress with streaming response parsing
130+
xhr.onprogress = function (event) {
131+
if (eventHandler) {
132+
const progressEvent = {
133+
event: {
134+
target: {
135+
responseText: xhr.responseText,
136+
},
137+
},
138+
progress: event.lengthComputable ? event.loaded / event.total : 0,
139+
};
140+
eventHandler(progressEvent);
141+
}
142+
};
143+
144+
// Handle abort controller
145+
controller.signal.addEventListener("abort", () => {
146+
xhr.abort();
147+
});
148+
149+
// Configure and send request
150+
xhr.open("POST", getBackendURL(env.UPLOAD));
151+
xhr.setRequestHeader("Reflex-Client-Token", getToken());
152+
xhr.setRequestHeader("Reflex-Event-Handler", handler);
153+
for (const [key, value] of Object.entries(extra_headers || {})) {
154+
xhr.setRequestHeader(key, value);
155+
}
156+
157+
try {
158+
xhr.send(formdata);
159+
} catch (error) {
160+
reject(error);
161+
}
162+
})
163+
.catch((error) => {
164+
console.log("Upload error:", error.message);
165+
return false;
166+
})
167+
.finally(() => {
168+
delete refs[upload_ref_name];
169+
});
170+
};

reflex/.templates/web/utils/state.js

Lines changed: 5 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import {
2020
} from "$/utils/context";
2121
import debounce from "$/utils/helpers/debounce";
2222
import throttle from "$/utils/helpers/throttle";
23+
import { uploadFiles } from "$/utils/helpers/upload";
2324

2425
// Endpoint URLs.
2526
const EVENTURL = env.EVENT;
26-
const UPLOADURL = env.UPLOAD;
2727

2828
// These hostnames indicate that the backend and frontend are reachable via the same domain.
2929
const SAME_DOMAIN_HOSTNAMES = ["localhost", "0.0.0.0", "::", "0:0:0:0:0:0:0:0"];
@@ -432,7 +432,11 @@ export const applyRestEvent = async (event, socket, navigate, params) => {
432432
event.payload.files,
433433
event.payload.upload_id,
434434
event.payload.on_upload_progress,
435+
event.payload.extra_headers,
435436
socket,
437+
refs,
438+
getBackendURL,
439+
getToken,
436440
);
437441
return false;
438442
}
@@ -614,163 +618,6 @@ export const connect = async (
614618
document.addEventListener("visibilitychange", checkVisibility);
615619
};
616620

617-
/**
618-
* Upload files to the server.
619-
*
620-
* @param state The state to apply the delta to.
621-
* @param handler The handler to use.
622-
* @param upload_id The upload id to use.
623-
* @param on_upload_progress The function to call on upload progress.
624-
* @param socket the websocket connection
625-
*
626-
* @returns The response from posting to the UPLOADURL endpoint.
627-
*/
628-
export const uploadFiles = async (
629-
handler,
630-
files,
631-
upload_id,
632-
on_upload_progress,
633-
socket,
634-
) => {
635-
// return if there's no file to upload
636-
if (files === undefined || files.length === 0) {
637-
return false;
638-
}
639-
640-
const upload_ref_name = `__upload_controllers_${upload_id}`;
641-
642-
if (refs[upload_ref_name]) {
643-
console.log("Upload already in progress for ", upload_id);
644-
return false;
645-
}
646-
647-
// Track how many partial updates have been processed for this upload.
648-
let resp_idx = 0;
649-
const eventHandler = (progressEvent) => {
650-
const event_callbacks = socket._callbacks.$event;
651-
// Whenever called, responseText will contain the entire response so far.
652-
const chunks = progressEvent.event.target.responseText.trim().split("\n");
653-
// So only process _new_ chunks beyond resp_idx.
654-
chunks.slice(resp_idx).map((chunk_json) => {
655-
try {
656-
const chunk = JSON5.parse(chunk_json);
657-
event_callbacks.map((f, ix) => {
658-
f(chunk)
659-
.then(() => {
660-
if (ix === event_callbacks.length - 1) {
661-
// Mark this chunk as processed.
662-
resp_idx += 1;
663-
}
664-
})
665-
.catch((e) => {
666-
if (progressEvent.progress === 1) {
667-
// Chunk may be incomplete, so only report errors when full response is available.
668-
console.log("Error processing chunk", chunk, e);
669-
}
670-
return;
671-
});
672-
});
673-
} catch (e) {
674-
if (progressEvent.progress === 1) {
675-
console.log("Error parsing chunk", chunk_json, e);
676-
}
677-
return;
678-
}
679-
});
680-
};
681-
682-
const controller = new AbortController();
683-
const formdata = new FormData();
684-
685-
// Add the token and handler to the file name.
686-
files.forEach((file) => {
687-
formdata.append("files", file, file.path || file.name);
688-
});
689-
690-
// Send the file to the server.
691-
refs[upload_ref_name] = controller;
692-
693-
return new Promise((resolve, reject) => {
694-
const xhr = new XMLHttpRequest();
695-
696-
// Set up event handlers
697-
xhr.onload = function () {
698-
if (xhr.status >= 200 && xhr.status < 300) {
699-
resolve({
700-
data: xhr.responseText,
701-
status: xhr.status,
702-
statusText: xhr.statusText,
703-
headers: {
704-
get: (name) => xhr.getResponseHeader(name),
705-
},
706-
});
707-
} else {
708-
reject(new Error(`HTTP error! status: ${xhr.status}`));
709-
}
710-
};
711-
712-
xhr.onerror = function () {
713-
reject(new Error("Network error"));
714-
};
715-
716-
xhr.onabort = function () {
717-
reject(new Error("Upload aborted"));
718-
};
719-
720-
// Handle upload progress
721-
if (on_upload_progress) {
722-
xhr.upload.onprogress = function (event) {
723-
if (event.lengthComputable) {
724-
const progressEvent = {
725-
loaded: event.loaded,
726-
total: event.total,
727-
progress: event.loaded / event.total,
728-
};
729-
on_upload_progress(progressEvent);
730-
}
731-
};
732-
}
733-
734-
// Handle download progress with streaming response parsing
735-
xhr.onprogress = function (event) {
736-
if (eventHandler) {
737-
const progressEvent = {
738-
event: {
739-
target: {
740-
responseText: xhr.responseText,
741-
},
742-
},
743-
progress: event.lengthComputable ? event.loaded / event.total : 0,
744-
};
745-
eventHandler(progressEvent);
746-
}
747-
};
748-
749-
// Handle abort controller
750-
controller.signal.addEventListener("abort", () => {
751-
xhr.abort();
752-
});
753-
754-
// Configure and send request
755-
xhr.open("POST", getBackendURL(UPLOADURL));
756-
xhr.setRequestHeader("Reflex-Client-Token", getToken());
757-
xhr.setRequestHeader("Reflex-Event-Handler", handler);
758-
759-
try {
760-
xhr.send(formdata);
761-
} catch (error) {
762-
reject(error);
763-
}
764-
})
765-
.catch((error) => {
766-
console.log("Upload error:", error.message);
767-
return false;
768-
})
769-
.finally(() => {
770-
delete refs[upload_ref_name];
771-
});
772-
};
773-
774621
/**
775622
* Create an event object.
776623
* @param {string} name The name of the event.

reflex/environment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def interpret_env_var_value(
204204
The interpreted value.
205205
206206
Raises:
207-
ValueError: If the value is invalid.
207+
ValueError: If the environment variable type is invalid.
208208
"""
209209
field_type = value_inside_optional(field_type)
210210

reflex/event.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ class FileUpload:
846846

847847
upload_id: str | None = None
848848
on_upload_progress: EventHandler | Callable | None = None
849+
extra_headers: dict[str, str] | None = None
849850

850851
@staticmethod
851852
def on_upload_progress_args_spec(_prog: Var[dict[str, int | float | bool]]):
@@ -887,6 +888,12 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec:
887888
Var(_js_expr="upload_id"),
888889
LiteralVar.create(upload_id),
889890
),
891+
(
892+
Var(_js_expr="extra_headers"),
893+
LiteralVar.create(
894+
self.extra_headers if self.extra_headers is not None else {}
895+
),
896+
),
890897
]
891898
if self.on_upload_progress is not None:
892899
on_upload_progress = self.on_upload_progress

0 commit comments

Comments
 (0)