Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/utils/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,13 @@ export function createProxyAgent(proxy?: ProxySettings, forUrl?: URL) {
// ::1 is served. Separate family: 4 and family: 6 lookups do not have these problems. Native
// Happy Eyeballs (autoSelectFamily) then races connection attempts across the families.
const dualStackLookup: net.LookupFunction = (hostname, options, callback) => {
const families = options.family === 4 || options.family === 6 ? [options.family] : [6, 4];
// Honor --dns-result-order=ipv4first, e.g. on networks where IPv6 is configured but not routed.
const defaultFamilies = dns.getDefaultResultOrder() === 'ipv4first' ? [4, 6] : [6, 4];
const families = options.family === 4 || options.family === 6 ? [options.family] : defaultFamilies;
void Promise.allSettled(families.map(family => dns.promises.lookup(hostname, { all: true, family }))).then(results => {
const perFamily = results.map(result => result.status === 'fulfilled' ? result.value : []);
const addresses: dns.LookupAddress[] = [];
// Alternate IPv6 and IPv4 addresses per RFC 8305 (prefer IPv6 first).
// Alternate address families per RFC 8305.
for (let i = 0; i < Math.max(...perFamily.map(list => list.length)); i++) {
for (const list of perFamily) {
if (list[i])
Expand Down
31 changes: 31 additions & 0 deletions tests/library/network-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import dns from 'dns';
import http from 'http';
import { browserTest as test, expect } from '../config/browserTest';
import { utils } from '../../packages/playwright-core/lib/coreBundle.js';
Expand Down Expand Up @@ -64,3 +65,33 @@ test('httpRequest should honor socketTimeout for stalled responses', async () =>
server.close();
}
});

test('httpRequest should honor ipv4first result order', async () => {
const server = http.createServer((req, res) => res.end('ok'));
await new Promise<void>(f => server.listen(0, '127.0.0.1', () => f()));
const port = (server.address() as import('node:net').AddressInfo).port;
const originalLookup = dns.promises.lookup;
const originalOrder = dns.getDefaultResultOrder();
// Emulate a host whose IPv6 address is not routable: connecting to it stalls.
(dns.promises as any).lookup = async (hostname: string, options: dns.LookupOptions) => {
if (hostname !== 'dual-stack.test')
return originalLookup(hostname, options as dns.LookupAllOptions);
return options.family === 6 ? [{ address: '203.0.113.1', family: 4 }] : [{ address: '127.0.0.1', family: 4 }];
};
dns.setDefaultResultOrder('ipv4first');
try {
const start = Date.now();
const status = await new Promise<number | Error>(resolve => {
utils.httpRequest({
url: `http://dual-stack.test:${port}/`,
socketTimeout: 30000,
}, response => resolve(response.statusCode!), resolve);
});
expect(status).toBe(200);
expect(Date.now() - start).toBeLessThan(2000);
} finally {
dns.setDefaultResultOrder(originalOrder);
dns.promises.lookup = originalLookup;
server.close();
}
});
Loading