diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts index 58436e7f9..d895ea61d 100644 --- a/crates/librqbit/webui/src/api-types.ts +++ b/crates/librqbit/webui/src/api-types.ts @@ -114,6 +114,7 @@ export interface ErrorDetails { status?: number; statusText?: string; text: string | React.ReactNode; + timedOut: boolean; } export type Duration = number; @@ -193,7 +194,8 @@ export interface RqbitAPI { ) => string | null; uploadTorrent: ( data: string | File, - opts?: AddTorrentOptions, + opts: AddTorrentOptions, + timeout?: number | undefined ) => Promise; pause: (index: number) => Promise; diff --git a/crates/librqbit/webui/src/components/buttons/UploadButton.tsx b/crates/librqbit/webui/src/components/buttons/UploadButton.tsx index 751830bab..be5eda4ad 100644 --- a/crates/librqbit/webui/src/components/buttons/UploadButton.tsx +++ b/crates/librqbit/webui/src/components/buttons/UploadButton.tsx @@ -31,13 +31,20 @@ export const UploadButton: React.FC<{ let t = setTimeout(async () => { setLoading(true); try { - const response = await API.uploadTorrent(data, { list_only: true }); + const response = await API.uploadTorrent(data, { list_only: true }, 2_000); setListTorrentResponse(response); - } catch (e) { - setListTorrentError({ - text: "Error listing torrent files", - details: e as ApiErrorDetails, - }); + } catch (e: unknown) { + let error = e as ApiErrorDetails; + if (error.timedOut) { + setListTorrentResponse(null); + // Timeout is not an error for a listOnly request + setListTorrentError(null); + } else { + setListTorrentError({ + text: "Error listing torrent files", + details: error, + }); + } } finally { setLoading(false); } diff --git a/crates/librqbit/webui/src/components/modal/FileSelectionModal.tsx b/crates/librqbit/webui/src/components/modal/FileSelectionModal.tsx index 0a41f336f..fb75a744f 100644 --- a/crates/librqbit/webui/src/components/modal/FileSelectionModal.tsx +++ b/crates/librqbit/webui/src/components/modal/FileSelectionModal.tsx @@ -52,16 +52,18 @@ export const FileSelectionModal = (props: { }; const handleUpload = async () => { - if (!listTorrentResponse) { - return; - } + // TODO this comment refers to the desired state, not the current state + // TODO implement listTorrentResponse==null support + // If listTorrentResponse is null, that indicates that the data is not available at the moment + // This is typically the case for magnet links that may not have dht peers setUploading(true); - let initialPeers = listTorrentResponse.seen_peers + let initialPeers = listTorrentResponse?.seen_peers ? listTorrentResponse.seen_peers.slice(0, 32) : null; + let only_files = selectedFiles.size <= 0 ? null : Array.from(selectedFiles); let opts: AddTorrentOptions = { overwrite: true, - only_files: Array.from(selectedFiles), + only_files, initial_peers: initialPeers, output_folder: outputFolder, }; diff --git a/crates/librqbit/webui/src/http-api.ts b/crates/librqbit/webui/src/http-api.ts index 5db88c211..fcf72979a 100644 --- a/crates/librqbit/webui/src/http-api.ts +++ b/crates/librqbit/webui/src/http-api.ts @@ -1,4 +1,5 @@ import { + AddTorrentOptions, AddTorrentResponse, ErrorDetails, ListTorrentsResponse, @@ -20,11 +21,39 @@ const apiUrl = (() => { return ""; })(); +// Wrapper around `fetch` to support a custom timeout. +// If specified, uses `AbortController` to timeout the pending fetch +function fetchWithTimeout(url: string, options: RequestInit = {}, timeout: number | undefined) { + let pending; + let timeoutId = undefined; + if (timeout !== undefined) { + const controller = new AbortController(); + const { signal } = controller; + + timeoutId = setTimeout(() => controller.abort(), timeout); + console.log("Fetching with timeout: ", timeout); + pending = fetch(url, { ...options, signal }) + } else { + pending = fetch(url, options) + } + + return pending + .then(response => { + clearTimeout(timeoutId); + return response; + }) + .catch(error => { + clearTimeout(timeoutId); + throw error; + }); +} + const makeRequest = async ( method: string, path: string, data?: any, isJson?: boolean, + timeout?: number, ): Promise => { console.log(method, path); const url = apiUrl + path; @@ -48,13 +77,21 @@ const makeRequest = async ( method: method, path: path, text: "", + timedOut: false, }; let response: Response; try { - response = await fetch(url, options); - } catch (e) { + response = await fetchWithTimeout(url, options, timeout); + } catch (e: any) { + console.log(e); + if (e.name === "AbortError") { + error.text = "fetch timed out after " + timeout + "ms" + error.timedOut = true; + return Promise.reject(error); + } + // else, generic network error error.text = "network error"; return Promise.reject(error); } @@ -92,8 +129,8 @@ export const API: RqbitAPI & { getVersion: () => Promise } = { stats: (): Promise => { return makeRequest("GET", "/stats"); }, - - uploadTorrent: (data, opts): Promise => { + uploadTorrent: (data: string | File, opts: AddTorrentOptions, timeout: number | undefined): Promise => { + console.log("Uploading torrent with ", data, opts, timeout); let url = "/torrents?&overwrite=true"; if (opts?.list_only) { url += "&list_only=true"; @@ -116,7 +153,7 @@ export const API: RqbitAPI & { getVersion: () => Promise } = { if (typeof data === "string") { url += "&is_url=true"; } - return makeRequest("POST", url, data); + return makeRequest("POST", url, data, undefined, timeout); }, updateOnlyFiles: (index: number, files: number[]): Promise => { diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 2542cce45..d9fc25b98 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -49,7 +49,10 @@ export const RqbitWebUI = (props: { ); setTorrents(torrents.torrents); }; - setRefreshTorrents(refreshTorrents); + + useEffect(() => { + setRefreshTorrents(refreshTorrents); + }, []) const setStats = useStatsStore((state) => state.setStats);