-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Differential download must fail after 10 seconds #9063
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
Comments
#9064 cannot fix this bug. |
It appears that there is a 10-second timeout, and the server has not responded. You can manually test whether your server supports range requests, or if it times out when using range requests. curl -v -L -H "Range: bytes=0-1023,2048-3071" https://www.yourserver.com/youapp.exe |
My server should be normal, the test results are as follows: If the download process doesn't exceed 10s, the differential download is normal. |
It shouldn’t be that, as downloading many large apps will definitely exceed 10 seconds. Currently, the timeout is set only after receiving the const requestOptions = differentialDownloader.createRequestOptions()
requestOptions.headers!.Range = ranges.substring(0, ranges.length - 2)
const request = differentialDownloader.httpExecutor.createRequest(requestOptions, response => {
if (!checkIsRangesSupported(response, reject)) {
return
}
const contentType = safeGetHeader(response, "content-type")
const m = /^multipart\/.+?\s*;\s*boundary=(?:"([^"]+)"|([^\s";]+))\s*$/i.exec(contentType)
if (m == null) {
reject(new Error(`Content-Type "multipart/byteranges" is expected, but got "${contentType}"`))
return
}
const dicer = new DataSplitter(out, options, partIndexToTaskIndex, m[1] || m[2], partIndexToLength, resolve)
dicer.on("error", reject)
response.pipe(dicer)
response.on("end", () => {
setTimeout(() => {
request.abort()
reject(new Error("Response ends without calling any handlers"))
}, 10000)
})
})
differentialDownloader.httpExecutor.addErrorAndTimeoutHandlers(request, reject)
request.end() What kind of server are you using? Nginx? Can you share the configuration so I can take a look? |
It seems like every Range response has an end event when I tested it. My server is built based on openresty.
|
Okay, here's how you can configure Nginx to support multiple HTTP Range requests: Nginx supports single HTTP Range requests (e.g., Add the Here's an example configuration within a server {
listen 80;
server_name your_domain.com; # Replace with your domain
root /var/www/html; # Replace with your document root
location / {
# ... other location directives ...
# Set the maximum number of allowed ranges
# 0: Disables multipart/byteranges
# 1 (or default): Allows only a single range
# >1: Allows multiple ranges, up to this number
max_ranges 1000; # Allow up to 1000 ranges
# Ensure Nginx can serve the files directly
try_files $uri $uri/ =404;
}
# ... other server directives ...
} Explanation:
After modifying the configuration, remember to reload or restart the Nginx service for the changes to take effect: sudo nginx -t # Test configuration syntax
sudo systemctl reload nginx # Reload configuration (recommended)
# or
sudo systemctl restart nginx # Restart Nginx service |
The result is still the same.
I've tried different nginx.conf.
|
You can try setting |
In addition, from the following source code, we can see that each HTTP Range response has an "end" event.
|
After setting max_ranges to 200000, the problem is solved. |
https://nginx.org/en/docs/http/ngx_http_core_module.html#max_ranges The use of max_ranges is somewhat inconsistent with the description. |
Then I’m not sure. It might be related to the version of Nginx you’re using. Since you’re using OpenResty, you can try switching to the latest official Nginx version and see if that helps. |
Sorry, I made a mistake. When max_ranges is 200000, the differential download time does not exceed 10s. |
However, the server running locally on your machine shouldn’t exceed 10 seconds, right? It still feels like there might be an issue with your configuration, or perhaps the server’s performance is too poor? I tested it locally with nginx and didn’t encounter any issues. Moreover, we are already using it in our production environment with AWS CDN, and we haven’t encountered the issue you mentioned. |
Thanks for your help. Maybe you are right, I will check my server and configuration again. |
I reproduced the bug on Cherry Studio(1.2.7->1.2.9). Here are the steps:
|
It indeed looks like there is an issue. The |
…#9064) The `Content-Type` response is as follows: `Content-Type: multipart/byteranges; boundary=799ddd5a-943e-4e22-981d-e32596ca39d2`. The `boundary` can have a space before it or no space at all. Here's the rfc: https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.1  **Issue** Currently, the check only accounts for cases where there is a space. If some servers return a response without a space, it will cause incremental downloads to fail. **Error Info** ``` 17:13:29.256 > Full: 92,770.98 KB, To download: 19,742.1 KB (21%) 17:13:29.633 > Cannot download differentially, fallback to full download: Error: Content-Type "multipart/byteranges" is expected, but got "multipart/byteranges;boundary=e888c103-165d-4ec1-9b6f-eca722ac9b10" at ClientRequest.<anonymous> (C:\Users\payne\AppData\Local\Programs\Cherry Studio\resources\app.asar\node_modules\electron-updater\out\differentialDownloader\multipleRangeDownloader.js:80:20) at ClientRequest.emit (node:events:531:35) at SimpleURLLoaderWrapper.<anonymous> (node:electron/js2c/browser_init:2:114720) at SimpleURLLoaderWrapper.emit (node:events:519:28) 17:13:34.288 > New version 1.2.9 has been downloaded to C:\Users\payne\AppData\Local\cherrystudio-updater\pending\Cherry-Studio-1.2.9-x64-setup.exe ``` **How to fix** ``` Welcome to Node.js v22.14.0. Type ".help" for more information. > /^multipart\/.+?\s*;\s*boundary=(?:"([^"]+)"|([^\s";]+))\s*$/i.exec("multipart/byteranges;boundary=799ddd5a-943e-4e22-981d-e32596ca39d2") [ 'multipart/byteranges;boundary=799ddd5a-943e-4e22-981d-e32596ca39d2', undefined, '799ddd5a-943e-4e22-981d-e32596ca39d2', index: 0, input: 'multipart/byteranges;boundary=799ddd5a-943e-4e22-981d-e32596ca39d2', groups: undefined ] > /^multipart\/.+?\s*;\s*boundary=(?:"([^"]+)"|([^\s";]+))\s*$/i.exec("multipart/byteranges; boundary=799ddd5a-943e-4e22-981d-e32596ca39d2") [ 'multipart/byteranges; boundary=799ddd5a-943e-4e22-981d-e32596ca39d2', undefined, '799ddd5a-943e-4e22-981d-e32596ca39d2', index: 0, input: 'multipart/byteranges; boundary=799ddd5a-943e-4e22-981d-e32596ca39d2', groups: undefined ] ``` might fix #9063
Any plans to fix this bug? |
@tessro Replace all the content in "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeTasksUsingMultipleRangeRequests = executeTasksUsingMultipleRangeRequests;
exports.checkIsRangesSupported = checkIsRangesSupported;
const builder_util_runtime_1 = require("builder-util-runtime");
const DataSplitter_1 = require("./DataSplitter");
const downloadPlanBuilder_1 = require("./downloadPlanBuilder");
function executeTasksUsingMultipleRangeRequests(differentialDownloader, tasks, out, oldFileFd, reject) {
const w = (taskOffset) => {
if (taskOffset >= tasks.length) {
if (differentialDownloader.fileMetadataBuffer != null) {
out.write(differentialDownloader.fileMetadataBuffer);
}
out.end();
return;
}
const nextOffset = taskOffset + 1000;
doExecuteTasks(differentialDownloader, {
tasks,
start: taskOffset,
end: Math.min(tasks.length, nextOffset),
oldFileFd,
}, out, () => w(nextOffset), reject);
};
return w;
}
function doExecuteTasks(differentialDownloader, options, out, resolve, reject) {
let ranges = "bytes=";
let partCount = 0;
const partIndexToTaskIndex = new Map();
const partIndexToLength = [];
for (let i = options.start; i < options.end; i++) {
const task = options.tasks[i];
if (task.kind === downloadPlanBuilder_1.OperationKind.DOWNLOAD) {
ranges += `${task.start}-${task.end - 1}, `;
partIndexToTaskIndex.set(partCount, i);
partCount++;
partIndexToLength.push(task.end - task.start);
}
}
if (partCount <= 1) {
// the only remote range - copy
const w = (index) => {
if (index >= options.end) {
resolve();
return;
}
const task = options.tasks[index++];
if (task.kind === downloadPlanBuilder_1.OperationKind.COPY) {
(0, DataSplitter_1.copyData)(task, out, options.oldFileFd, reject, () => w(index));
}
else {
const requestOptions = differentialDownloader.createRequestOptions();
requestOptions.headers.Range = `bytes=${task.start}-${task.end - 1}`;
const request = differentialDownloader.httpExecutor.createRequest(requestOptions, response => {
if (!checkIsRangesSupported(response, reject)) {
return;
}
response.pipe(out, {
end: false,
});
response.once("end", () => w(index));
});
differentialDownloader.httpExecutor.addErrorAndTimeoutHandlers(request, reject);
request.end();
}
};
w(options.start);
return;
}
const requestOptions = differentialDownloader.createRequestOptions();
requestOptions.headers.Range = ranges.substring(0, ranges.length - 2);
let timeoutId = null;
const wrappedResolve = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
resolve();
};
const wrappedReject = (error) => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
reject(error);
};
const request = differentialDownloader.httpExecutor.createRequest(requestOptions, response => {
if (!checkIsRangesSupported(response, wrappedReject)) {
return;
}
const contentType = (0, builder_util_runtime_1.safeGetHeader)(response, "content-type");
const m = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i.exec(contentType);
if (m == null) {
wrappedReject(new Error(`Content-Type "multipart/byteranges" is expected, but got "${contentType}"`));
return;
}
const dicer = new DataSplitter_1.DataSplitter(out, options, partIndexToTaskIndex, m[1] || m[2], partIndexToLength, wrappedResolve);
dicer.on("error", wrappedReject);
response.pipe(dicer);
response.on("end", () => {
timeoutId = setTimeout(() => {
timeoutId = null;
request.abort();
reject(new Error("Response ends without calling any handlers"));
}, 30000);
});
});
differentialDownloader.httpExecutor.addErrorAndTimeoutHandlers(request, wrappedReject);
request.end();
}
function checkIsRangesSupported(response, reject) {
// Electron net handles redirects automatically, our NodeJS test server doesn't use redirects - so, we don't check 3xx codes.
if (response.statusCode >= 400) {
reject((0, builder_util_runtime_1.createHttpError)(response));
return false;
}
if (response.statusCode !== 206) {
const acceptRanges = (0, builder_util_runtime_1.safeGetHeader)(response, "accept-ranges");
if (acceptRanges == null || acceptRanges === "none") {
reject(new Error(`Server doesn't support Accept-Ranges (response code ${response.statusCode})`));
return false;
}
}
return true;
}
//# sourceMappingURL=multipleRangeDownloader.js.map% |
When useMultipleRangeRequest is true, differential download must fail after 10 seconds.
DifferentialDownloader.ts:
multipleRangeDownloader.ts:
The text was updated successfully, but these errors were encountered: