From d8f0c4602c14dc719b2e67efcf7011b11bb34957 Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:52:49 +0000 Subject: [PATCH] chore: introduce HttpError and show status in result --- src/error/HttpError.ts | 11 +++++++++++ src/error/index.ts | 1 + src/utils/Fetcher.test.ts | 13 +++++++++---- src/utils/Fetcher.ts | 4 ++-- src/utils/StreamResolver.test.ts | 12 +++++++++++- src/utils/StreamResolver.ts | 7 ++++++- .../__snapshots__/StreamResolver.test.ts.snap | 16 ++++++++++++++++ 7 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 src/error/HttpError.ts diff --git a/src/error/HttpError.ts b/src/error/HttpError.ts new file mode 100644 index 0000000..2e29be0 --- /dev/null +++ b/src/error/HttpError.ts @@ -0,0 +1,11 @@ +export class HttpError extends Error { + public readonly status: number; + public readonly headers: Record; + + constructor(status: number, headers: Record) { + super(); + + this.status = status; + this.headers = headers; + } +} diff --git a/src/error/index.ts b/src/error/index.ts index c6fe528..374ebaa 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,3 +1,4 @@ export * from './BlockedError'; +export * from './HttpError'; export * from './NotFoundError'; export * from './QueueIsFullError'; diff --git a/src/utils/Fetcher.test.ts b/src/utils/Fetcher.test.ts index ae1f064..49d9121 100644 --- a/src/utils/Fetcher.test.ts +++ b/src/utils/Fetcher.test.ts @@ -2,7 +2,7 @@ import winston from 'winston'; import fetchMock from 'fetch-mock'; import { Fetcher } from './Fetcher'; import { Context } from '../types'; -import { BlockedError, NotFoundError, QueueIsFullError } from '../error'; +import { BlockedError, HttpError, NotFoundError, QueueIsFullError } from '../error'; fetchMock.mockGlobal(); const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] })); @@ -143,11 +143,16 @@ describe('fetch', () => { } }); - test('passes through other errors with detailed infos', async () => { + test('passes through other error as HttpError', async () => { fetchMock.get('https://some-error-url.test/', { status: 500, headers: { 'x-foo': 'bar' } }); - await expect(fetcher.text(ctx, new URL('https://some-error-url.test/'))).rejects - .toThrow('Fetcher error: 500: Internal Server Error, response headers: {"content-length":"0","x-foo":"bar","age":"0","date":"Wed, 11 Apr 1990 12:34:56 GMT"}'); + try { + await fetcher.text(ctx, new URL('https://some-error-url.test/')); + fail(); + } catch (error) { + expect(error).toBeInstanceOf(HttpError); + expect(error).toMatchObject({ status: 500, headers: { 'x-foo': 'bar' } }); + } }); test('passes through exceptions', async () => { diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts index 1b61e58..10414eb 100644 --- a/src/utils/Fetcher.ts +++ b/src/utils/Fetcher.ts @@ -4,7 +4,7 @@ import winston from 'winston'; import { Mutex } from 'async-mutex'; import { Cookie, CookieJar } from 'tough-cookie'; import { Context, TIMEOUT } from '../types'; -import { BlockedError, NotFoundError, QueueIsFullError } from '../error'; +import { BlockedError, HttpError, NotFoundError, QueueIsFullError } from '../error'; import { clearTimeout } from 'node:timers'; import { envGet } from './env'; @@ -157,7 +157,7 @@ export class Fetcher { throw new BlockedError('unknown', httpCacheItem.policy.responseHeaders()); } - throw new Error(`Fetcher error: ${httpCacheItem.status}: ${httpCacheItem.statusText}, response headers: ${JSON.stringify(responseHeaders)}`); + throw new HttpError(httpCacheItem.status, responseHeaders); }; private readonly determineTtl = (httpCacheItem: HttpCacheItem): number => { diff --git a/src/utils/StreamResolver.test.ts b/src/utils/StreamResolver.test.ts index 8b8c3fb..ea8b7d5 100644 --- a/src/utils/StreamResolver.test.ts +++ b/src/utils/StreamResolver.test.ts @@ -4,7 +4,7 @@ import { StreamResolver } from './StreamResolver'; import { Handler, MeineCloud, MostraGuarda } from '../handler'; import { Fetcher } from './Fetcher'; import { Context, CountryCode, TIMEOUT, UrlResult } from '../types'; -import { BlockedError, NotFoundError, QueueIsFullError } from '../error'; +import { BlockedError, HttpError, NotFoundError, QueueIsFullError } from '../error'; jest.mock('../utils/Fetcher'); const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }); @@ -113,6 +113,16 @@ describe('resolve', () => { countryCode: 'de', }, }, + { + url: new URL('https://example4.com'), + isExternal: true, + error: new HttpError(500, { 'x-foo': 'bar' }), + label: 'hoster.com', + sourceId: '', + meta: { + countryCode: 'de', + }, + }, ]; }; } diff --git a/src/utils/StreamResolver.ts b/src/utils/StreamResolver.ts index 3ce1039..0dbdaa7 100644 --- a/src/utils/StreamResolver.ts +++ b/src/utils/StreamResolver.ts @@ -4,7 +4,7 @@ import winston from 'winston'; import bytes from 'bytes'; import { Context, TIMEOUT, UrlResult } from '../types'; import { Handler } from '../handler'; -import { BlockedError, NotFoundError, QueueIsFullError } from '../error'; +import { BlockedError, HttpError, NotFoundError, QueueIsFullError } from '../error'; import { languageFromCountryCode } from './languageFromCountryCode'; import { envGetAppName } from './env'; @@ -165,6 +165,11 @@ export class StreamResolver { return '⏳ Request queue is full. Please try again later or consider self-hosting.'; } + if (error instanceof HttpError) { + this.logger.error(`${source}: HTTP status ${error.status}, headers: ${JSON.stringify(error.headers)}.`, ctx); + return `❌ Request failed with status ${error.status}. Request-id: ${ctx.id}.`; + } + const cause = (error as Error & { cause?: unknown }).cause; this.logger.error(`${source} error: ${error}, cause: ${cause}`, ctx); diff --git a/src/utils/__snapshots__/StreamResolver.test.ts.snap b/src/utils/__snapshots__/StreamResolver.test.ts.snap index 9c9d2c6..aec952a 100644 --- a/src/utils/__snapshots__/StreamResolver.test.ts.snap +++ b/src/utils/__snapshots__/StreamResolver.test.ts.snap @@ -43,6 +43,14 @@ exports[`resolve adds error info 1`] = ` 🔗 hoster.com ⏳ Request queue is full. Please try again later or consider self-hosting.", }, + { + "behaviorHints": {}, + "externalUrl": "https://example4.com/", + "name": "WebStreamr N/A ⚠️ external", + "title": "🌐 German 🇩🇪 +🔗 hoster.com +❌ Request failed with status 500. Request-id: id.", + }, ], } `; @@ -90,6 +98,14 @@ exports[`resolve adds error info 2`] = ` ⏳ Request queue is full. Please try again later or consider self-hosting.", "ytId": "E4WlUXrJgy4", }, + { + "behaviorHints": {}, + "name": "WebStreamr N/A", + "title": "🌐 German 🇩🇪 +🔗 hoster.com +❌ Request failed with status 500. Request-id: id.", + "ytId": "E4WlUXrJgy4", + }, ], } `;