Skip to content

Commit a075b49

Browse files
JohnMcLearclaude
andcommitted
fix(export): validate rev before attachment, broaden error catch
Qodo feedback on #7792: 1) ExportHandler.doExport set Content-Disposition (via res.attachment) before calling checkValidRev. If the rev was invalid, the route-level catch returned a plain-text 500 — but the attachment header was still in place, so browsers offered to save the error message as a file. Move checkValidRev to the top of doExport so an invalid rev never touches the attachment header. 2) The catch only converted CustomError('...', 'apierror') into a plain-text response. Other export errors (conversion failures, fs issues, soffice problems) still fell through to Express's default HTML renderer — non-deterministic for API callers and confusing when they were already half-downloading a file. Surface every export failure as a deterministic text/plain 500. apierrors carry user-facing messages, so send err.message verbatim. For other errors, log the full stack server-side and still emit err.message (or 'Internal Server Error' if absent) so the response body is never the Express HTML stack page. Also clear Content-Disposition in the catch as a safety net. Backend tests (importexportGetPost.ts): 58 passing, 0 failing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b537f16 commit a075b49

2 files changed

Lines changed: 25 additions & 13 deletions

File tree

src/node/handler/ExportHandler.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ const tempDirectory = os.tmpdir();
4444
* @param {String} type the type to export
4545
*/
4646
exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string, type:string) => {
47+
// Validate :rev BEFORE setting Content-Disposition. A bad rev causes
48+
// checkValidRev to throw, which the route handler catches and renders as a
49+
// plain-text 500. If we set the attachment header first, the browser would
50+
// download the error message as a file instead of displaying it.
51+
if (req.params.rev !== undefined) {
52+
// modify req, as we use it in a later call to exportConvert
53+
req.params.rev = checkValidRev(req.params.rev);
54+
}
55+
4756
// avoid naming the read-only file as the original pad's id
4857
let fileName = readOnlyId ? readOnlyId : padId;
4958

@@ -58,12 +67,6 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
5867
// tell the browser that this is a downloadable file
5968
res.attachment(`${fileName}.${type}`);
6069

61-
if (req.params.rev !== undefined) {
62-
// ensure revision is a number
63-
// modify req, as we use it in a later call to exportConvert
64-
req.params.rev = checkValidRev(req.params.rev);
65-
}
66-
6770
// if this is a plain text export, we can do this directly
6871
// We have to over engineer this because tabs are stored as attributes and not plain text
6972
if (type === 'etherpad') {

src/node/hooks/express/importexport.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,22 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
7272
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
7373
}
7474
})().catch((err) => {
75-
// checkValidRev throws CustomError('...', 'apierror') for non-numeric or
76-
// out-of-range :rev. Surface the message as a plain-text body so callers
77-
// see why the request failed instead of Express's default HTML page.
78-
if (err && err.name === 'apierror' && !res.headersSent) {
79-
return res.status(500).type('text/plain').send(err.message);
80-
}
81-
return next(err || new Error(err));
75+
// Send a deterministic plain-text body for every export failure.
76+
// checkValidRev throws CustomError('...', 'apierror') for a bad :rev,
77+
// but conversion / fs / soffice errors also reach this handler — and
78+
// without an explicit response, all of them would fall through to
79+
// Express's default HTML error renderer, which is hostile to API
80+
// callers (and would be saved as a file by the browser because of
81+
// the attachment header set in doExport for non-apierror cases).
82+
if (res.headersSent) return next(err || new Error(err));
83+
// Clear the download header so the error body renders inline instead
84+
// of being saved as the requested filename.
85+
res.removeHeader('Content-Disposition');
86+
// Log the full error server-side for operators. apierrors are
87+
// user-facing validation errors and not worth a server-side log line.
88+
if (!err || err.name !== 'apierror') console.error('Export error:', err);
89+
const msg = (err && err.message) || 'Internal Server Error';
90+
return res.status(500).type('text/plain').send(msg);
8291
});
8392
});
8493

0 commit comments

Comments
 (0)