Skip to content

Commit feeb9d6

Browse files
committed
Add timeout to downloading databases
1 parent 58b26d2 commit feeb9d6

File tree

3 files changed

+86
-12
lines changed

3 files changed

+86
-12
lines changed

extensions/ql-vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@
376376
"title": "Adding databases",
377377
"order": 6,
378378
"properties": {
379+
"codeQL.addingDatabases.downloadTimeout": {
380+
"type": "integer",
381+
"default": 10,
382+
"description": "Download timeout in seconds for adding a CodeQL database."
383+
},
379384
"codeQL.addingDatabases.allowHttp": {
380385
"type": "boolean",
381386
"default": false,

extensions/ql-vscode/src/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,16 @@ const DEPRECATED_ALLOW_HTTP_SETTING = new Setting(
644644

645645
const ADDING_DATABASES_SETTING = new Setting("addingDatabases", ROOT_SETTING);
646646

647+
const DOWNLOAD_TIMEOUT_SETTING = new Setting(
648+
"downloadTimeout",
649+
ADDING_DATABASES_SETTING,
650+
);
647651
const ALLOW_HTTP_SETTING = new Setting("allowHttp", ADDING_DATABASES_SETTING);
648652

653+
export function downloadTimeout(): number {
654+
return DOWNLOAD_TIMEOUT_SETTING.getValue<number>() || 10;
655+
}
656+
649657
export function allowHttp(): boolean {
650658
return (
651659
ALLOW_HTTP_SETTING.getValue<boolean>() ||

extensions/ql-vscode/src/databases/database-fetcher.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Response } from "node-fetch";
2-
import fetch from "node-fetch";
2+
import fetch, { AbortError } from "node-fetch";
33
import { zip } from "zip-a-folder";
44
import type { InputBoxOptions } from "vscode";
55
import { Uri, window } from "vscode";
@@ -28,11 +28,16 @@ import {
2828
} from "../common/github-url-identifier-helper";
2929
import type { Credentials } from "../common/authentication";
3030
import type { AppCommandManager } from "../common/commands";
31-
import { addDatabaseSourceToWorkspace, allowHttp } from "../config";
31+
import {
32+
addDatabaseSourceToWorkspace,
33+
allowHttp,
34+
downloadTimeout,
35+
} from "../config";
3236
import { showAndLogInformationMessage } from "../common/logging";
3337
import { AppOctokit } from "../common/octokit";
3438
import { getLanguageDisplayName } from "../common/query-language";
3539
import type { DatabaseOrigin } from "./local-databases/database-origin";
40+
import { clearTimeout } from "node:timers";
3641

3742
/**
3843
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -478,10 +483,38 @@ async function fetchAndUnzip(
478483
step: 1,
479484
});
480485

481-
const response = await checkForFailingResponse(
482-
await fetch(databaseUrl, { headers: requestHeaders }),
483-
"Error downloading database",
484-
);
486+
const abortController = new AbortController();
487+
488+
const timeout = downloadTimeout() * 1000;
489+
490+
let timeoutId: NodeJS.Timeout;
491+
492+
// If we don't get any data within the timeout, abort the download
493+
timeoutId = setTimeout(() => {
494+
abortController.abort();
495+
}, timeout);
496+
497+
let response: Response;
498+
try {
499+
response = await checkForFailingResponse(
500+
await fetch(databaseUrl, {
501+
headers: requestHeaders,
502+
signal: abortController.signal,
503+
}),
504+
"Error downloading database",
505+
);
506+
} catch (e) {
507+
clearTimeout(timeoutId);
508+
509+
if (e instanceof AbortError) {
510+
const thrownError = new AbortError("The request timed out.");
511+
thrownError.stack = e.stack;
512+
throw thrownError;
513+
}
514+
515+
throw e;
516+
}
517+
485518
const archiveFileStream = createWriteStream(archivePath);
486519

487520
const contentLength = response.headers.get("content-length");
@@ -493,12 +526,40 @@ async function fetchAndUnzip(
493526
progress,
494527
);
495528

496-
await new Promise((resolve, reject) =>
497-
response.body
498-
.pipe(archiveFileStream)
499-
.on("finish", resolve)
500-
.on("error", reject),
501-
);
529+
// If we receive any data within the timeout, reset the timeout
530+
response.body.on("data", () => {
531+
clearTimeout(timeoutId);
532+
timeoutId = setTimeout(() => {
533+
abortController.abort();
534+
}, timeout);
535+
});
536+
537+
try {
538+
await new Promise((resolve, reject) => {
539+
response.body
540+
.pipe(archiveFileStream)
541+
.on("finish", resolve)
542+
.on("error", reject);
543+
544+
// If an error occurs on the body, we also want to reject the promise (e.g. during a timeout error).
545+
response.body.on("error", reject);
546+
});
547+
} catch (e) {
548+
// Close and remove the file if an error occurs
549+
archiveFileStream.close(() => {
550+
void remove(archivePath);
551+
});
552+
553+
if (e instanceof AbortError) {
554+
const thrownError = new AbortError("The download timed out.");
555+
thrownError.stack = e.stack;
556+
throw thrownError;
557+
}
558+
559+
throw e;
560+
} finally {
561+
clearTimeout(timeoutId);
562+
}
502563

503564
await readAndUnzip(
504565
Uri.file(archivePath).toString(true),

0 commit comments

Comments
 (0)