fix: honor Node all-address DNS lookup for HTTP webhooks
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-14 14:03:00 +08:00
parent 40c8e279f0
commit 92b112cc6e
9 changed files with 281 additions and 2 deletions
@@ -0,0 +1,42 @@
import { createServer, get, type RequestOptions } from 'node:http';
import type { AddressInfo } from 'node:net';
import { pinnedWebhookLookup } from './open-api.protocol';
describe('webhook pinned DNS lookup', () => {
it('supports single-address and all-address callbacks without resolving another address', () => {
const callback = jest.fn();
const lookup = pinnedWebhookLookup('203.0.113.10', 4);
lookup('ignored.example', {}, callback);
expect(callback).toHaveBeenLastCalledWith(null, '203.0.113.10', 4);
lookup('ignored.example', { all: true }, callback);
expect(callback).toHaveBeenLastCalledWith(null, [{ address: '203.0.113.10', family: 4 }]);
pinnedWebhookLookup('2001:db8::10', 6)('ignored.example', { all: true }, callback);
expect(callback).toHaveBeenLastCalledWith(null, [{ address: '2001:db8::10', family: 6 }]);
});
it('delivers through the real Node HTTP connector when automatic family selection requests all addresses', async () => {
const server = createServer((_request, response) => response.end('received'));
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const port = (server.address() as AddressInfo).port;
const options: RequestOptions & { autoSelectFamily: boolean } = {
lookup: pinnedWebhookLookup('127.0.0.1', 4),
autoSelectFamily: true,
agent: false,
};
const body = await new Promise<string>((resolve, reject) => {
const request = get(`http://webhook.invalid:${port}/`, options, (response) => {
let received = '';
response.setEncoding('utf8');
response.on('data', (chunk: string) => (received += chunk));
response.on('end', () => resolve(received));
});
request.setTimeout(3000, () => request.destroy(new Error('test HTTP timeout')));
request.on('error', reject);
});
expect(body).toBe('received');
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
});
+9
View File
@@ -1,5 +1,14 @@
import { createHash, createHmac } from 'node:crypto';
import { HttpException } from '@nestjs/common';
import type { LookupFunction } from 'node:net';
/** Keep the validated address pinned while honoring Node's all-address lookup contract. */
export function pinnedWebhookLookup(address: string, family: number): LookupFunction {
return (_hostname, options, callback) => {
if (options.all) callback(null, [{ address, family }]);
else callback(null, address, family);
};
}
/** v1 compatibility: an absent parsed body hashes as {}, never try alternate hashes. */
export function openApiBodyHash(rawBody: Buffer | undefined, body: unknown) {
+2 -2
View File
@@ -26,7 +26,7 @@ import { SendChainService } from '../send-chain/send-chain.service';
import { decryptSecret, encryptSecret } from './open-api.crypto';
import type { OpenApiAuthContext } from './open-api.types';
import { ProtocolLogsService } from '../protocol-logs/protocol-logs.service';
import { publicOpenApiFailure, webhookJobId } from './open-api.protocol';
import { pinnedWebhookLookup, publicOpenApiFailure, webhookJobId } from './open-api.protocol';
import { automaticDeliveryMode } from './delivery-mode';
export const OPEN_API_WEBHOOK_TRANSPORT = Symbol('open-api-webhook-transport');
@@ -934,7 +934,7 @@ async function postWebhook(
{
method: 'POST',
headers: { ...headers, 'content-length': String(Buffer.byteLength(body)) },
lookup: (_hostname, _options, callback) => callback(null, target.address, target.family),
lookup: pinnedWebhookLookup(target.address, target.family),
},
(response) => {
const chunks: Buffer[] = [];