diff --git a/app/gen-server/lib/DocApiForwarder.ts b/app/gen-server/lib/DocApiForwarder.ts index 6cd8dc1e..3e62d2a6 100644 --- a/app/gen-server/lib/DocApiForwarder.ts +++ b/app/gen-server/lib/DocApiForwarder.ts @@ -10,6 +10,7 @@ import { IDocWorkerMap } from "app/server/lib/DocWorkerMap"; import { expressWrap } from "app/server/lib/expressWrap"; import { GristServer } from "app/server/lib/GristServer"; import { getAssignmentId } from "app/server/lib/idUtils"; +import { addAbortHandler } from "app/server/lib/requestUtils"; /** * Forwards all /api/docs/:docId/tables requests to the doc worker handling the :docId document. Makes @@ -101,7 +102,7 @@ export class DocApiForwarder { // If the original request is aborted, abort the forwarded request too. (Currently this only // affects some export/download requests which can abort long-running work.) - req.on('close', () => controller.abort()); + addAbortHandler(req, res, () => controller.abort()); const options: RequestInit = { method: req.method, diff --git a/app/server/lib/ExportXLSX.ts b/app/server/lib/ExportXLSX.ts index 1beb2dc3..3a2a065b 100644 --- a/app/server/lib/ExportXLSX.ts +++ b/app/server/lib/ExportXLSX.ts @@ -14,6 +14,7 @@ import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {ActiveDocSource, ActiveDocSourceDirect, DownloadOptions, ExportParameters} from 'app/server/lib/Export'; import log from 'app/server/lib/log'; +import {addAbortHandler} from 'app/server/lib/requestUtils'; import * as express from 'express'; import contentDisposition from 'content-disposition'; import {Rpc} from 'grain-rpc'; @@ -78,7 +79,7 @@ export async function streamXLSX(activeDoc: ActiveDoc, req: express.Request, req.off('close', cancelWorker); }); - req.on('close', cancelWorker); + addAbortHandler(req, outputStream, cancelWorker); const run = (method: string, ...args: any[]) => exportPool.run({port: port2, testDates, args}, { name: method, diff --git a/app/server/lib/requestUtils.ts b/app/server/lib/requestUtils.ts index b2922c23..522de9d1 100644 --- a/app/server/lib/requestUtils.ts +++ b/app/server/lib/requestUtils.ts @@ -9,6 +9,7 @@ import log from 'app/server/lib/log'; import {Permit} from 'app/server/lib/Permit'; import {Request, Response} from 'express'; import _ from 'lodash'; +import {Writable} from 'stream'; // log api details outside of dev environment (when GRIST_HOSTED_VERSION is set) const shouldLogApiDetails = Boolean(process.env.GRIST_HOSTED_VERSION); @@ -335,3 +336,19 @@ export function clearSessionCacheIfNeeded(req: Request, options?: { }) { (req as RequestWithGrist).gristServer?.getSessions().clearCacheIfNeeded(options); } + +export function addAbortHandler(req: Request, res: Writable, op: () => void) { + // It became hard to detect aborted connections in node 16. + // In node 14, req.on('close', ...) did the job. + // The following is a work-around, until a better way is discovered + // or added. Aborting a req will typically lead to 'close' being called + // on the response, without writableFinished being set. + // https://github.com/nodejs/node/issues/38924 + // https://github.com/nodejs/node/issues/40775 + res.on('close', () => { + const aborted = !res.writableFinished; + if (aborted) { + op(); + } + }); +}