11import type { Response } from "node-fetch" ;
2- import fetch from "node-fetch" ;
2+ import fetch , { AbortError } from "node-fetch" ;
33import { zip } from "zip-a-folder" ;
44import type { InputBoxOptions } from "vscode" ;
55import { Uri , window } from "vscode" ;
@@ -28,11 +28,16 @@ import {
2828} from "../common/github-url-identifier-helper" ;
2929import type { Credentials } from "../common/authentication" ;
3030import type { AppCommandManager } from "../common/commands" ;
31- import { addDatabaseSourceToWorkspace , allowHttp } from "../config" ;
31+ import {
32+ addDatabaseSourceToWorkspace ,
33+ allowHttp ,
34+ downloadTimeout ,
35+ } from "../config" ;
3236import { showAndLogInformationMessage } from "../common/logging" ;
3337import { AppOctokit } from "../common/octokit" ;
3438import { getLanguageDisplayName } from "../common/query-language" ;
3539import 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