fix: 拒绝插件重定向HTTP错误状态

This commit is contained in:
sunlei 2026-06-19 18:32:11 +08:00
parent fc12e608a6
commit 87522a4326
4 changed files with 32 additions and 7 deletions

View File

@ -361,7 +361,7 @@ function getHttpRequestOptions(
function getRedirectRequestOptions(
args: Record<string, unknown>,
): 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');
}

View File

@ -16,6 +16,7 @@ export type QqbotPluginHttpClientRequest = {
export type QqbotPluginResolveRedirectRequest = {
context?: string;
failureMessage?: (statusCode: number) => string;
headers?: Record<string, string>;
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 &&

View File

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

View File

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