chore: introduce HttpError and show status in result

This commit is contained in:
WebStreamr 2025-06-05 19:52:49 +00:00
parent 119e338292
commit d8f0c4602c
No known key found for this signature in database
7 changed files with 56 additions and 8 deletions

11
src/error/HttpError.ts Normal file
View file

@ -0,0 +1,11 @@
export class HttpError extends Error {
public readonly status: number;
public readonly headers: Record<string, string[] | string | undefined>;
constructor(status: number, headers: Record<string, string[] | string | undefined>) {
super();
this.status = status;
this.headers = headers;
}
}

View file

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

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, 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 () => {

View file

@ -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 => {

View file

@ -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',
},
},
];
};
}

View file

@ -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);

View file

@ -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",
},
],
}
`;