refactor: avoid leaking fetch internals by introducing TimeoutError

This commit is contained in:
WebStreamr 2025-06-17 15:42:17 +00:00
parent aa9085c263
commit e1f5192f4b
No known key found for this signature in database
7 changed files with 19 additions and 13 deletions

View file

@ -0,0 +1 @@
export class TimeoutError extends Error {}

View file

@ -3,3 +3,4 @@ export * from './HttpError';
export * from './NotFoundError';
export * from './QueueIsFullError';
export * from './TooManyRequestsError';
export * from './TimeoutError';

View file

@ -1,7 +1,5 @@
import { Manifest, ManifestConfig } from 'stremio-addon-sdk';
export const TIMEOUT = 'timeout';
export interface Context {
id: string;
ip: string;

View file

@ -2,7 +2,7 @@ import winston from 'winston';
import fetchMock from 'fetch-mock';
import { Fetcher } from './Fetcher';
import { Context } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TooManyRequestsError } from '../error';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TimeoutError, TooManyRequestsError } from '../error';
fetchMock.mockGlobal();
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
@ -182,7 +182,7 @@ describe('fetch', () => {
test('times out', async () => {
fetchMock.get('https://some-timeout-url.test/', 200, { delay: 20 });
await expect(fetcher.text(ctx, new URL('https://some-timeout-url.test/'), { timeout: 10 })).rejects.toBeInstanceOf(DOMException);
await expect(fetcher.text(ctx, new URL('https://some-timeout-url.test/'), { timeout: 10 })).rejects.toBeInstanceOf(TimeoutError);
});
test('queues requests and throws if queue is full', async () => {

View file

@ -3,8 +3,8 @@ import TTLCache from '@isaacs/ttlcache';
import winston from 'winston';
import { Semaphore, SemaphoreInterface, withTimeout } from 'async-mutex';
import { Cookie, CookieJar } from 'tough-cookie';
import { BlockedReason, Context, TIMEOUT } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TooManyRequestsError } from '../error';
import { BlockedReason, Context } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TimeoutError, TooManyRequestsError } from '../error';
import { clearTimeout } from 'node:timers';
import { envGet } from './env';
@ -229,11 +229,17 @@ export class Fetcher {
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(TIMEOUT), init?.timeout ?? this.DEFAULT_TIMEOUT);
const timer = setTimeout(() => controller.abort(), init?.timeout ?? this.DEFAULT_TIMEOUT);
let response;
try {
response = await fetch(url, { ...init, keepalive: true, signal: controller.signal });
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
throw new TimeoutError();
}
throw error;
} finally {
clearTimeout(timer);
}

View file

@ -3,8 +3,8 @@ import winston from 'winston';
import { createExtractors, Extractor, ExtractorRegistry } from '../extractor';
import { StreamResolver } from './StreamResolver';
import { Source, SourceResult, MeineCloud, MostraGuarda } from '../source';
import { BlockedReason, Context, CountryCode, TIMEOUT, UrlResult } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TooManyRequestsError } from '../error';
import { BlockedReason, Context, CountryCode, UrlResult } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TimeoutError, TooManyRequestsError } from '../error';
import { ImdbId } from './id';
import { FetcherMock } from './FetcherMock';
@ -128,7 +128,7 @@ describe('resolve', () => {
{
url: new URL('https://example2.com'),
isExternal: true,
error: TIMEOUT,
error: new TimeoutError(),
label: 'hoster.com',
sourceId: '',
meta: {

View file

@ -1,9 +1,9 @@
import { ContentType, Stream } from 'stremio-addon-sdk';
import winston from 'winston';
import bytes from 'bytes';
import { Context, TIMEOUT, UrlResult } from '../types';
import { Context, UrlResult } from '../types';
import { Source } from '../source';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TooManyRequestsError } from '../error';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TimeoutError, TooManyRequestsError } from '../error';
import { flagFromCountryCode, languageFromCountryCode } from './language';
import { envGetAppName } from './env';
import { Id } from './id';
@ -169,7 +169,7 @@ export class StreamResolver {
return '🚦 Request was rate-limited. Please try again later or consider self-hosting.';
}
if (error === TIMEOUT) {
if (error instanceof TimeoutError) {
this.logger.warn(`${source}: Request timed out.`, ctx);
return '🐢 Request timed out.';