Rewrite http utils; properly handle (& retry on) network errors
This commit is contained in:
parent
132adcb733
commit
97267ef89a
3 changed files with 54 additions and 40 deletions
|
@ -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(
|
||||
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<IncomingMessage>((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<Buffer>((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);
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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/, "");
|
||||
|
|
Loading…
Reference in a new issue