fix: range headers in file.js
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled

Range header support was mostly incorrect, which brought to surface
undefined behavior in OnlyOffice which resulted in a request hanging.
This was the cause of the `pdf-editor` app hanging for a while when
opening it with a new file.
This commit is contained in:
KernelDeimos
2026-02-02 19:56:09 -05:00
committed by Eric Dubé
parent e2e4794bbc
commit 5250671b01
2 changed files with 90 additions and 89 deletions
+84 -89
View File
@@ -125,111 +125,106 @@ router.get('/file', async (req, res, next) => {
});
const fileSize = fsentry[0].size;
//--------------------------------------------------
// No range
//--------------------------------------------------
if ( ! range ) {
// set content-type, if available
if ( contentType !== null ) {
res.setHeader('Content-Type', contentType);
res.setHeader('Accept-Ranges', 'bytes');
const parseRangeHeader = (rangeHeader) => {
// Check if this is a multipart range request
if ( rangeHeader.includes(',') ) {
// For now, we'll only serve the first range in multipart requests
// as the underlying storage layer doesn't support multipart responses
const firstRange = rangeHeader.split(',')[0].trim();
const matches = firstRange.match(/bytes=(\d+)-(\d*)/);
if ( ! matches ) return null;
const start = parseInt(matches[1], 10);
const end = matches[2] ? parseInt(matches[2], 10) : null;
return { start, end, isMultipart: true };
}
const svc_filesystem = req.services.get('filesystem');
// Single range request
const matches = rangeHeader.match(/bytes=(\d+)-(\d*)/);
if ( ! matches ) return null;
// stream data from S3
try {
/* eslint-disable */
const fsNode = await svc_filesystem.node(
new NodeRawEntrySelector(fsentry[0]),
);
/* eslint-enable */
const ll_read = new LLRead();
const stream = await ll_read.run({
no_acl: true,
actor: req.actor ?? ownerActor,
fsNode,
});
const start = parseInt(matches[1], 10);
const end = matches[2] ? parseInt(matches[2], 10) : null;
return { start, end, isMultipart: false };
};
return stream.pipe(res);
} catch (e) {
errors.report('read from storage', {
source: e,
trace: true,
alarm: true,
});
return res.type('application/json').status(500).send({ message: 'There was an internal problem reading the file.' });
}
}
//--------------------------------------------------
// Range
//--------------------------------------------------
else {
const total = fsentry[0].size;
const user_agent = req.get('User-Agent');
if ( range ) {
res.status(206);
const rangeInfo = parseRangeHeader(req.headers['range']);
if ( rangeInfo ) {
const { start, end, isMultipart } = rangeInfo;
let start, end, chunkSize = 5000000;
let is_safari = false;
// For open-ended ranges, we need to calculate the actual end byte
let actualEnd = end;
let fileSize = null;
// Parse range header
var parts = range.replace(/bytes=/, '').split('-');
var partialstart = parts[0];
var partialend = parts[1];
try {
fileSize = fsentry[0].size;
if ( end === null ) {
actualEnd = fileSize - 1; // File size is 1-based, end byte is 0-based
}
} catch (e) {
// If we can't get file size, we'll let the storage layer handle it
// and not set Content-Range header
actualEnd = null;
fileSize = null;
}
start = parseInt(partialstart, 10);
end = partialend ? parseInt(partialend, 10) : total - 1;
if ( actualEnd !== null ) {
const totalSize = fileSize !== null ? fileSize : '*';
const contentRange = `bytes ${start}-${actualEnd}/${totalSize}`;
res.set('Content-Range', contentRange);
}
if ( user_agent && user_agent.toLowerCase().includes('safari') && !user_agent.includes('Chrome') ) {
// Safari
is_safari = true;
chunkSize = (end - start) + 1;
} else {
// All other user agents
end = Math.min(start + chunkSize, fileSize - 1);
// If this was a multipart request, modify the range header to only include the first range
if ( isMultipart ) {
req.headers['range'] = end !== null
? `bytes=${start}-${end}`
: `bytes=${start}-`;
}
}
}
// Create headers
const headers = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': is_safari ? chunkSize : (end - start + 1),
};
//--------------------------------------------------
// No range
//--------------------------------------------------
// set content-type, if available
if ( contentType !== null ) {
res.setHeader('Content-Type', contentType);
}
// Set Content-Type, if available
if ( contentType ) {
headers['Content-Type'] = contentType;
}
const svc_filesystem = req.services.get('filesystem');
// HTTP Status 206 for Partial Content
res.writeHead(206, headers);
// stream data from S3
try {
/* eslint-disable */
const fsNode = await svc_filesystem.node(
new NodeRawEntrySelector(fsentry[0]),
);
/* eslint-enable */
const ll_read = new LLRead();
const stream = await ll_read.run({
range,
no_acl: true,
actor: req.actor ?? ownerActor,
fsNode,
});
try {
const svc_filesystem = req.services.get('filesystem');
/* eslint-disable */
const fsNode = await svc_filesystem.node(
new NodeRawEntrySelector(fsentry[0]),
);
/* eslint-enable */
const ll_read = new LLRead();
const stream = await ll_read.run({
no_acl: true,
actor: req.actor ?? ownerActor,
fsNode,
...(req.headers['range'] ? { range: req.headers['range'] } : { }),
});
// const storage = Context.get('storage');
// let stream = await storage.create_read_stream(fsentry[0].uuid, {
// bucket: fsentry[0].bucket,
// bucket_region: fsentry[0].bucket_region,
// });
return stream.pipe(res);
} catch (e) {
errors.report('read from storage', {
source: e,
trace: true,
alarm: true,
});
return res.type('application/json').status(500).send({ message: 'There was an internal problem reading the file.' });
}
return stream.pipe(res);
} catch (e) {
errors.report('read from storage', {
source: e,
trace: true,
alarm: true,
});
return res.type('application/json').status(500).send({ message: 'There was an internal problem reading the file.' });
}
});
@@ -0,0 +1,6 @@
{
"key": "test-webhook-66999928605bc47b",
"webhook_secret": "9c193c4e111780a42b3d27661779adebba03c132078847490eb61639ce73288c",
"nonce": 13,
"instance_url": "http://api.puter.localhost:4100"
}