From 97267ef89a24f4fefae256481bf9bd8380a1d3d4 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 12 Mar 2024 02:31:46 +0100 Subject: [PATCH] Rewrite http utils; properly handle (& retry on) network errors --- src/main/utils/http.ts | 77 +++++++++++++++++++-------------- src/main/utils/vencordLoader.ts | 15 ++++--- src/updater/main.ts | 2 +- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts index 9a98384..baee81e 100644 --- a/src/main/utils/http.ts +++ b/src/main/utils/http.ts @@ -5,41 +5,54 @@ */ import { createWriteStream } from "fs"; -import type { IncomingMessage } from "http"; -import { get, RequestOptions } from "https"; -import { finished } from "stream/promises"; +import { Readable } from "stream"; +import { pipeline } from "stream/promises"; +import { setTimeout } from "timers/promises"; -export async function downloadFile(url: string, file: string, options: RequestOptions = {}) { - const res = await simpleReq(url, options); - await finished( - res.pipe( - createWriteStream(file, { - autoClose: true - }) - ) +interface FetchieOptions { + retryOnNetworkError?: boolean; +} + +export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) { + const res = await fetchie(url, options, fetchieOpts); + await pipeline( + // @ts-expect-error odd type error + Readable.fromWeb(res.body!), + createWriteStream(file, { + autoClose: true + }) ); } -export function simpleReq(url: string, options: RequestOptions = {}) { - return new Promise((resolve, reject) => { - get(url, options, res => { - const { statusCode, statusMessage, headers } = res; - if (statusCode! >= 400) return void reject(`${statusCode}: ${statusMessage} - ${url}`); - if (statusCode! >= 300) return simpleReq(headers.location!, options).then(resolve).catch(reject); +const ONE_MINUTE_MS = 1000 * 60; - resolve(res); - }); - }); -} - -export async function simpleGet(url: string, options: RequestOptions = {}) { - const res = await simpleReq(url, options); - - return new Promise((resolve, reject) => { - const chunks = [] as Buffer[]; - - res.once("error", reject); - res.on("data", chunk => chunks.push(chunk)); - res.once("end", () => resolve(Buffer.concat(chunks))); - }); +export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) { + let res: Response | undefined; + + try { + res = await fetch(url, options); + } catch (err) { + if (retryOnNetworkError) { + console.error("Failed to fetch", url + ".", "Gonna retry with backoff."); + + for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) { + await setTimeout(delayMs); + try { + res = await fetch(url, options); + break; + } catch {} + } + } + + if (!res) throw new Error(`Failed to fetch ${url}\n${err}`); + } + + if (res.ok) return res; + + let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`; + + const reason = await res.text().catch(() => ""); + if (reason) msg += `\n${reason}`; + + throw new Error(msg); } diff --git a/src/main/utils/vencordLoader.ts b/src/main/utils/vencordLoader.ts index 293654a..1bac37a 100644 --- a/src/main/utils/vencordLoader.ts +++ b/src/main/utils/vencordLoader.ts @@ -5,11 +5,10 @@ */ import { existsSync, mkdirSync } from "fs"; -import type { RequestOptions } from "https"; import { join } from "path"; import { USER_AGENT, VENCORD_FILES_DIR } from "../constants"; -import { downloadFile, simpleGet } from "./http"; +import { downloadFile, fetchie } from "./http"; const API_BASE = "https://api.github.com"; @@ -31,27 +30,29 @@ export interface ReleaseData { } export async function githubGet(endpoint: string) { - const opts: RequestOptions = { + const opts: RequestInit = { headers: { Accept: "application/vnd.github+json", "User-Agent": USER_AGENT } }; - if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; - return simpleGet(API_BASE + endpoint, opts); + return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true }); } export async function downloadVencordFiles() { const release = await githubGet("/repos/Vendicated/Vencord/releases/latest"); - const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData; + const { assets }: ReleaseData = await release.json(); await Promise.all( assets .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f))) - .map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name))) + .map(({ name, browser_download_url }) => + downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true }) + ) ); } diff --git a/src/updater/main.ts b/src/updater/main.ts index 059afb9..207687e 100644 --- a/src/updater/main.ts +++ b/src/updater/main.ts @@ -81,7 +81,7 @@ export async function checkUpdates() { try { const raw = await githubGet("/repos/Vencord/Vesktop/releases/latest"); - const data = JSON.parse(raw.toString("utf-8")) as ReleaseData; + const data: ReleaseData = await raw.json(); const oldVersion = app.getVersion(); const newVersion = data.tag_name.replace(/^v/, "");