Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/bright-laws-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"electron-updater": patch
---

fix(differentialDownloader): improve error handling and timeout management in multipleRangeDownloader
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ function doExecuteTasks(differentialDownloader: DifferentialDownloader, options:

const requestOptions = differentialDownloader.createRequestOptions()
requestOptions.headers!.Range = ranges.substring(0, ranges.length - 2)

let timeoutId: NodeJS.Timeout | null = null

const wrappedResolve = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId)
timeoutId = null
}
resolve()
}

const request = differentialDownloader.httpExecutor.createRequest(requestOptions, response => {
if (!checkIsRangesSupported(response, reject)) {
return
Expand All @@ -101,15 +112,16 @@ function doExecuteTasks(differentialDownloader: DifferentialDownloader, options:
return
}

const dicer = new DataSplitter(out, options, partIndexToTaskIndex, m[1] || m[2], partIndexToLength, resolve)
const dicer = new DataSplitter(out, options, partIndexToTaskIndex, m[1] || m[2], partIndexToLength, wrappedResolve)
dicer.on("error", reject)
response.pipe(dicer)

response.on("end", () => {
setTimeout(() => {
timeoutId = setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to store timeoutId for anything? Curious as to why we need to have wrappedReject. I was under the impression setTimeout without storing its ID would auto-release when GC'd

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reject does seem unnecessary now, as it has already failed. At the time of writing, it was done for consistency, so both resolve and reject were included.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleted.

Copy link
Collaborator

@mmaietta mmaietta May 1, 2025

Choose a reason for hiding this comment

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

I still see timeoutId but only being cleared on resolve now. Why can't we reuse previous logic and only change the timeout?

Are we simply detecting whether the timeout is working properly? e.g. are we attempting to resolve and reject the promise in the same flow so we're hitting a race condition?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@beyondkmp can you please follow up here when you have a chance? Then we can proceed with the PR

timeoutId = null
request.abort()
reject(new Error("Response ends without calling any handlers"))
}, 10000)
}, 30000)
})
})
differentialDownloader.httpExecutor.addErrorAndTimeoutHandlers(request, reject)
Expand Down