mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(server): give the last six outbound fetches a timeout, and lint for it
Overpass, the Wikipedia photo lookup (which holds one of five shared photo slots), the plugin download and registry, and the two API key checks had no signal, so a provider that took the connection and went quiet held the request for undici's 300 s default. ESLint now refuses a fetch() whose init carries no signal.
This commit is contained in:
@@ -21,6 +21,9 @@ import type { AppSettingsRepository } from '../../db/repositories/AppSettings.re
|
||||
import { Users } from '../../db/entities/Users.entity';
|
||||
import type { UsersRepository, UserApiKeyColumns, UserProfilePatch } from '../../db/repositories/Users.repository';
|
||||
|
||||
/** A key check is a single request to a provider; one that hangs must not hold the settings page. */
|
||||
const KEY_CHECK_TIMEOUT_MS = 10_000;
|
||||
|
||||
/**
|
||||
* The account a user administers about themselves: display settings, avatar,
|
||||
* the third-party API keys they paste in, and the directory listing other
|
||||
@@ -376,6 +379,7 @@ export class UserProfileService {
|
||||
'X-Goog-FieldMask': SEARCH_TEXT_FIELD_MASK,
|
||||
},
|
||||
body: JSON.stringify({ textQuery: 'test' }),
|
||||
signal: AbortSignal.timeout(KEY_CHECK_TIMEOUT_MS),
|
||||
}
|
||||
);
|
||||
result.maps = mapsRes.status === 200;
|
||||
@@ -412,7 +416,8 @@ export class UserProfileService {
|
||||
if (openweather_api_key) {
|
||||
try {
|
||||
const weatherRes = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${openweather_api_key}`
|
||||
`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${openweather_api_key}`,
|
||||
{ signal: AbortSignal.timeout(KEY_CHECK_TIMEOUT_MS) },
|
||||
);
|
||||
result.weather = weatherRes.status === 200;
|
||||
} catch {
|
||||
|
||||
@@ -1469,6 +1469,9 @@ export class MapsService {
|
||||
method: 'POST',
|
||||
headers: { 'User-Agent': UA, 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `data=${encodeURIComponent(query)}`,
|
||||
// The query asks Overpass for 5 s; a public instance that is overloaded
|
||||
// takes the connection and then sits on it.
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { elements?: OverpassElement[] };
|
||||
@@ -1615,7 +1618,12 @@ export class MapsService {
|
||||
pilimit: '1',
|
||||
redirects: '1',
|
||||
});
|
||||
const res = await fetch(`https://en.wikipedia.org/w/api.php?${searchParams}`, { headers: { 'User-Agent': UA } });
|
||||
const res = await fetch(`https://en.wikipedia.org/w/api.php?${searchParams}`, {
|
||||
headers: { 'User-Agent': UA },
|
||||
// This runs inside one of the few shared photo-fetch slots; a stalled
|
||||
// answer would hold it for everyone.
|
||||
signal: AbortSignal.timeout(WIKI_TIMEOUT_MS),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as { query?: { pages?: Record<string, { thumbnail?: { source?: string } }> } };
|
||||
const pages = data.query?.pages;
|
||||
|
||||
@@ -13,6 +13,8 @@ import { isBlockedIp } from '../runtime/egress-policy';
|
||||
|
||||
const MAX_REDIRECTS = 5;
|
||||
const MAX_BYTES = 50 * 1024 * 1024;
|
||||
/** The whole download, every hop and the body included. */
|
||||
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
||||
|
||||
export class DownloadError extends Error {}
|
||||
|
||||
@@ -51,9 +53,10 @@ async function assertSafeHost(urlStr: string): Promise<void> {
|
||||
|
||||
export async function safeDownload(urlStr: string, maxBytes = MAX_BYTES): Promise<{ bytes: Buffer; sha256: string }> {
|
||||
let current = urlStr;
|
||||
const signal = AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS);
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
await assertSafeHost(current);
|
||||
const resp = await fetch(current, { redirect: 'manual', headers: { 'User-Agent': 'TREK-Server' } });
|
||||
const resp = await fetch(current, { redirect: 'manual', headers: { 'User-Agent': 'TREK-Server' }, signal });
|
||||
if (resp.status >= 300 && resp.status < 400) {
|
||||
const loc = resp.headers.get('location');
|
||||
if (!loc) throw new DownloadError('redirect without a location');
|
||||
|
||||
@@ -220,7 +220,7 @@ export class PluginRegistryService {
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
headers.Pragma = 'no-cache';
|
||||
}
|
||||
const resp = await fetch(url, { headers });
|
||||
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(15_000) });
|
||||
if (!resp.ok) throw new Error(`registry ${resp.status}`);
|
||||
const data = (await resp.json()) as Registry;
|
||||
if (!data || !Array.isArray(data.plugins)) throw new Error('malformed registry');
|
||||
|
||||
Reference in New Issue
Block a user