From 51b9104f07f3f1af805e5920c54e6e6bcdb7b187 Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:35:52 +0000 Subject: [PATCH] chore: handle cloudflare 451 censor block, show block reasons --- src/types.ts | 1 + src/utils/Fetcher.test.ts | 13 ++++++++++ src/utils/Fetcher.ts | 4 +++ src/utils/StreamResolver.test.ts | 11 ++++++++ src/utils/StreamResolver.ts | 4 ++- .../__snapshots__/StreamResolver.test.ts.snap | 26 ++++++++++++++++--- 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/types.ts b/src/types.ts index 9a8ea70..097742e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,7 @@ export enum CountryCode { export enum BlockedReason { cloudflare_challenge = 'cloudflare_challenge', flaresolverr_failed = 'flaresolverr_failed', + cloudflare_censor = 'cloudflare_censor', media_flow_proxy_auth = 'media_flow_proxy_auth', unknown = 'unknown', } diff --git a/src/utils/Fetcher.test.ts b/src/utils/Fetcher.test.ts index 60a049e..80c7a77 100644 --- a/src/utils/Fetcher.test.ts +++ b/src/utils/Fetcher.test.ts @@ -149,6 +149,19 @@ describe('fetch', () => { } }); + test('converts cloudflare 451 to custom BlockedError', async () => { + const mockPool = mockAgent.get('https://some-cloudflare-forbidden-url.test'); + mockPool.intercept({ path: '/' }).reply(451); + + try { + await fetcher.text(ctx, new URL('https://some-cloudflare-forbidden-url.test/')); + fail(); + } catch (error) { + expect(error).toBeInstanceOf(BlockedError); + expect(error).toMatchObject({ reason: BlockedReason.cloudflare_censor }); + } + }); + test('converts MediaFlow Proxy forbidden to custom BlockedError', async () => { const mockPool = mockAgent.get('https://media-flow-proxy-forbidden-url.test'); mockPool.intercept({ path: '/' }).reply(403); diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts index c1f76cc..8e420df 100644 --- a/src/utils/Fetcher.ts +++ b/src/utils/Fetcher.ts @@ -171,6 +171,10 @@ export class Fetcher { throw new BlockedError(BlockedReason.unknown, httpCacheItem.policy.responseHeaders()); } + if (httpCacheItem.status === 451) { + throw new BlockedError(BlockedReason.cloudflare_censor, httpCacheItem.policy.responseHeaders()); + } + if (httpCacheItem.status === 429) { const retryAfter = parseInt(`${httpCacheItem.policy.responseHeaders()['retry-after']}`); if (!isNaN(retryAfter)) { diff --git a/src/utils/StreamResolver.test.ts b/src/utils/StreamResolver.test.ts index c456bdc..0248092 100644 --- a/src/utils/StreamResolver.test.ts +++ b/src/utils/StreamResolver.test.ts @@ -103,6 +103,17 @@ describe('resolve', () => { countryCodes: [CountryCode.de], }, }, + { + url: new URL('https://example.com'), + format: Format.unknown, + isExternal: true, + error: new BlockedError(BlockedReason.cloudflare_censor, {}), + label: 'hoster.com', + sourceId: '', + meta: { + countryCodes: [CountryCode.de], + }, + }, { url: new URL('https://example.com'), format: Format.unknown, diff --git a/src/utils/StreamResolver.ts b/src/utils/StreamResolver.ts index 2267c4d..fcdb6a8 100644 --- a/src/utils/StreamResolver.ts +++ b/src/utils/StreamResolver.ts @@ -175,13 +175,15 @@ export class StreamResolver { if (error instanceof BlockedError) { if (error.reason === BlockedReason.cloudflare_challenge) { this.logger.warn(`${source}: Request was blocked via Cloudflare challenge.`, ctx); + } else if (error.reason === BlockedReason.cloudflare_censor) { + this.logger.warn(`${source}: Request was censored by Cloudflare.`, ctx); } else if (error.reason === BlockedReason.media_flow_proxy_auth) { return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.'; } else { this.logger.warn(`${source}: Request was blocked, headers: ${JSON.stringify(error.headers)}.`, ctx); } - return '⚠️ Request was blocked.'; + return `⚠️ Request was blocked. Reason: ${error.reason}`; } if (error instanceof TooManyRequestsError) { diff --git a/src/utils/__snapshots__/StreamResolver.test.ts.snap b/src/utils/__snapshots__/StreamResolver.test.ts.snap index 5e40af2..a262aef 100644 --- a/src/utils/__snapshots__/StreamResolver.test.ts.snap +++ b/src/utils/__snapshots__/StreamResolver.test.ts.snap @@ -28,7 +28,16 @@ exports[`resolve adds error info 1`] = ` "externalUrl": "http://localhost/", "name": "WebStreamr 🇩🇪", "title": "🔗 hoster.com -⚠️ Request was blocked.", +⚠️ Request was blocked. Reason: cloudflare_challenge", + }, + { + "behaviorHints": { + "notWebReady": true, + }, + "externalUrl": "http://localhost/", + "name": "WebStreamr 🇩🇪", + "title": "🔗 hoster.com +⚠️ Request was blocked. Reason: cloudflare_censor", }, { "behaviorHints": { @@ -46,7 +55,7 @@ exports[`resolve adds error info 1`] = ` "externalUrl": "http://localhost/", "name": "WebStreamr 🇩🇪", "title": "🔗 hoster.com -⚠️ Request was blocked.", +⚠️ Request was blocked. Reason: unknown", }, { "behaviorHints": { @@ -143,7 +152,16 @@ exports[`resolve adds error info 2`] = ` "externalUrl": "https://example.com/", "name": "WebStreamr 🇩🇪 ⚠️ external", "title": "🔗 hoster.com -⚠️ Request was blocked.", +⚠️ Request was blocked. Reason: cloudflare_challenge", + }, + { + "behaviorHints": { + "notWebReady": true, + }, + "externalUrl": "https://example.com/", + "name": "WebStreamr 🇩🇪 ⚠️ external", + "title": "🔗 hoster.com +⚠️ Request was blocked. Reason: cloudflare_censor", }, { "behaviorHints": { @@ -161,7 +179,7 @@ exports[`resolve adds error info 2`] = ` "externalUrl": "https://example.com/", "name": "WebStreamr 🇩🇪 ⚠️ external", "title": "🔗 hoster.com -⚠️ Request was blocked.", +⚠️ Request was blocked. Reason: unknown", }, { "behaviorHints": {