diff --git a/src/modules/qqbot/plugin-platform/infrastructure/integration/runtime/plugin-host-bridge.service.ts b/src/modules/qqbot/plugin-platform/infrastructure/integration/runtime/plugin-host-bridge.service.ts index ead18a5..1ed5318 100644 --- a/src/modules/qqbot/plugin-platform/infrastructure/integration/runtime/plugin-host-bridge.service.ts +++ b/src/modules/qqbot/plugin-platform/infrastructure/integration/runtime/plugin-host-bridge.service.ts @@ -361,7 +361,7 @@ function getHttpRequestOptions( function getRedirectRequestOptions( args: Record, ): QqbotPluginResolveRedirectRequest { - const candidate = args.input || args.options || args; + const candidate = args.input ?? args.options ?? args; if (!isRecord(candidate)) { throw new Error('Plugin host redirect options must be an object'); } diff --git a/src/modules/qqbot/plugin-platform/infrastructure/integration/sdk/plugin-http-client.service.ts b/src/modules/qqbot/plugin-platform/infrastructure/integration/sdk/plugin-http-client.service.ts index 0a3c4a1..c72b462 100644 --- a/src/modules/qqbot/plugin-platform/infrastructure/integration/sdk/plugin-http-client.service.ts +++ b/src/modules/qqbot/plugin-platform/infrastructure/integration/sdk/plugin-http-client.service.ts @@ -16,6 +16,7 @@ export type QqbotPluginHttpClientRequest = { export type QqbotPluginResolveRedirectRequest = { context?: string; + failureMessage?: (statusCode: number) => string; headers?: Record; maxRedirects?: number; timeoutMessage?: string; @@ -194,7 +195,7 @@ export class QqbotPluginHttpClientService { * Requests one URL and returns its redirect Location after the response body is drained. * @param url - Validated HTTP(S) URL to request. * @param input - Headers and timeout options shared across the redirect chain. - * @returns Redirect Location header when the response is 3xx, otherwise `undefined`. + * @returns Redirect Location header when the response is 3xx, otherwise `undefined`; rejects for HTTP error statuses. */ private requestRedirectLocation( url: URL, @@ -222,6 +223,16 @@ export class QqbotPluginHttpClientService { response.on('error', reject); response.on('end', () => { + if (statusCode >= 400) { + reject( + createPluginHttpError( + input.failureMessage?.(statusCode) || + `${context}请求失败:${statusCode}`, + statusCode, + ), + ); + return; + } if ( statusCode >= 300 && statusCode < 400 && diff --git a/test/modules/qqbot/plugin-platform/plugin-host-bridge.spec.ts b/test/modules/qqbot/plugin-platform/plugin-host-bridge.spec.ts index 6630d80..9d82e8c 100644 --- a/test/modules/qqbot/plugin-platform/plugin-host-bridge.spec.ts +++ b/test/modules/qqbot/plugin-platform/plugin-host-bridge.spec.ts @@ -132,11 +132,11 @@ describe('QQBot plugin host bridge', () => { const options = { maxRedirects: 3, timeoutMs: 1000, - url: 'https://b23.tv/abc123', + url: 'https://short.example/abc123', }; httpClient.resolveRedirect.mockResolvedValue({ - finalUrl: 'https://www.bilibili.com/video/BV1xx411c7mD', - redirects: ['https://www.bilibili.com/video/BV1xx411c7mD'], + finalUrl: 'https://target.example/video/123', + redirects: ['https://target.example/video/123'], }); await expect( @@ -148,8 +148,8 @@ describe('QQBot plugin host bridge', () => { ).resolves.toEqual({ ok: true, value: { - finalUrl: 'https://www.bilibili.com/video/BV1xx411c7mD', - redirects: ['https://www.bilibili.com/video/BV1xx411c7mD'], + finalUrl: 'https://target.example/video/123', + redirects: ['https://target.example/video/123'], }, }); expect(httpClient.resolveRedirect).toHaveBeenCalledWith(options); diff --git a/test/modules/qqbot/plugin-platform/plugin-http-client.spec.ts b/test/modules/qqbot/plugin-platform/plugin-http-client.spec.ts index dbeb30b..b75f853 100644 --- a/test/modules/qqbot/plugin-platform/plugin-http-client.spec.ts +++ b/test/modules/qqbot/plugin-platform/plugin-http-client.spec.ts @@ -23,6 +23,11 @@ describe('QQBot plugin HTTP client redirect resolver', () => { response.end(); return; } + if (request.url === '/missing') { + response.writeHead(404, { 'Content-Type': 'text/plain' }); + response.end('missing'); + return; + } response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end('ok'); }); @@ -67,4 +72,13 @@ describe('QQBot plugin HTTP client redirect resolver', () => { }), ).rejects.toThrow('插件 HTTP 重定向仅支持 http/https'); }); + + it('rejects HTTP error statuses while resolving redirects', async () => { + await expect( + new QqbotPluginHttpClientService().resolveRedirect({ + timeoutMs: 1000, + url: `${baseUrl}/missing`, + }), + ).rejects.toMatchObject({ statusCode: 404 }); + }); });