-
Notifications
You must be signed in to change notification settings - Fork 4
Hotfix/file upload pooling #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,6 +31,42 @@ export class DropzoneJS extends React.Component { | |||||||||||||||||||||||||||||||||||||||||||||||
| this.props.onUploadComplete(response, this.props.id, this.props.data); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| pollUploadStatus(fileId, baseUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const statusUrl = `${baseUrl}/status/${fileId}`; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const maxAttempts = 300; // 10 minutes at 2s intervals | ||||||||||||||||||||||||||||||||||||||||||||||||
| let attempts = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| this._pollInterval = setInterval(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| attempts++; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (attempts > maxAttempts) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(this._pollInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this._pollInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.onError({ message: 'Upload timed out' }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const accessToken = await getAccessToken(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch(statusUrl, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Authorization': `Bearer ${accessToken}` } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (data.status === 'complete') { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(this._pollInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this._pollInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.onUploadComplete(data); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (data.status === 'error') { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(this._pollInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this._pollInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.onError(data); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(this._pollInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this._pollInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.onError(error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, 2000); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Configuration of Dropzone.js. Defaults are | ||||||||||||||||||||||||||||||||||||||||||||||||
| * overriden by the 'djsConfig' property | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -106,6 +142,10 @@ export class DropzoneJS extends React.Component { | |||||||||||||||||||||||||||||||||||||||||||||||
| * Removes dropzone.js (and all its globals) if the component is being unmounted | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| componentWillUnmount () { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (this._pollInterval) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(this._pollInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this._pollInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.dropzone) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const files = this.dropzone.getActiveFiles(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -278,6 +318,13 @@ export class DropzoneJS extends React.Component { | |||||||||||||||||||||||||||||||||||||||||||||||
| _this.onUploadComplete(uploadResponse); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| else if(xhr?.status == 202) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Async upload: server accepted the file, poll for completion | ||||||||||||||||||||||||||||||||||||||||||||||||
| let uploadResponse = JSON.parse(xhr.responseText); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const fileId = uploadResponse.file_id; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const baseUrl = _this.props.config.postUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||
| _this.pollUploadStatus(fileId, baseUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+321
to
+327
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate the 202 payload before starting the poll.
🛡️ Proposed guard- else if(xhr?.status == 202) {
- // Async upload: server accepted the file, poll for completion
- let uploadResponse = JSON.parse(xhr.responseText);
- const fileId = uploadResponse.file_id;
- const baseUrl = _this.props.config.postUrl;
- _this.pollUploadStatus(fileId, baseUrl);
- }
+ else if(xhr?.status == 202) {
+ // Async upload: server accepted the file, poll for completion
+ let uploadResponse;
+ try {
+ uploadResponse = JSON.parse(xhr.responseText);
+ } catch (parseErr) {
+ _this.onError(parseErr, xhr?.status);
+ return;
+ }
+ const fileId = uploadResponse?.file_id;
+ if (!fileId) {
+ _this.onError({ message: 'Async upload accepted (202) but response is missing file_id' }, xhr?.status);
+ return;
+ }
+ _this.pollUploadStatus(fileId, _this.props.config.postUrl);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| else{ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _this.onError(JSON.parse(xhr?.responseText), xhr?.status); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: OpenStackweb/openstack-uicore-foundation
Length of output: 23442
🏁 Script executed:
Repository: OpenStackweb/openstack-uicore-foundation
Length of output: 271
🏁 Script executed:
Repository: OpenStackweb/openstack-uicore-foundation
Length of output: 173
🏁 Script executed:
Repository: OpenStackweb/openstack-uicore-foundation
Length of output: 1208
🏁 Script executed:
Repository: OpenStackweb/openstack-uicore-foundation
Length of output: 1265
🏁 Script executed:
Repository: OpenStackweb/openstack-uicore-foundation
Length of output: 2284
Polling mechanism has several robustness gaps that will bite in production.
Concurrent uploads clobber
this._pollInterval. Dropzone.js defaults toparallelUploads: 2, allowing multiple files to upload in parallel. When both files receive HTTP 202 responses, both callpollUploadStatus, overwritingthis._pollInterval. The first file's interval is orphaned and keeps running, callingonUploadComplete/onErrorspuriously. Only the most recent interval is cleared on unmount. Use a map (this._pollIntervals[fileId]) to track one interval per file and clean all on unmount.Any transient failure aborts polling. A single network blip, a 5xx error, or a non-JSON response body (causing
response.json()to throw) immediately clears the interval and firesonError. For a 10‑minute async upload, transient failures are almost guaranteed. Treat request failures as retriable; only give up aftermaxAttemptsor an explicitstatus === 'error'.No
response.okcheck. A401/403(e.g., token expired mid-poll) or500with a JSON body is currently parsed as a normal status payload. Checkresponse.okbefore parsing and treat non-2xx responses as transient failures.setInterval+asyncoverlaps. IfgetAccessToken()+fetchtakes >2s (plausible under load), the next tick fires before the previous completes, issuing overlapping token requests and status calls. Use self-schedulingsetTimeoutrecursion so the next poll is scheduled only after the previous one resolves.♻️ Sketch of a safer polling loop
And update
componentWillUnmount:📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smarcet this seems a valid argument: Concurrent uploads clobber this._pollInterval. Dropzone.js defaults to parallelUploads: 2, allowing multiple files to upload in parallel. When both files receive HTTP 202 responses, both call pollUploadStatus, overwriting this._pollInterval. The first file's interval is orphaned and keeps running, calling onUploadComplete/onError spuriously. Only the most recent interval is cleared on unmount. Use a map (this._pollIntervals[fileId]) to track one interval per file and clean all on unmount.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.