Conversation
| return async function dnsReqRetryTimeout(lookupAddress: string) { | ||
| try { | ||
| return await dns.promises[api](lookupAddress); | ||
| return (await dns.promises.resolve(lookupAddress, rrtype)) as dns.SrvRecord[] | string[][]; | ||
| } catch (firstDNSError) { | ||
| if (firstDNSError.code === dns.TIMEOUT) { | ||
| return await dns.promises[api](lookupAddress); | ||
| return (await dns.promises.resolve(lookupAddress, rrtype)) as dns.SrvRecord[] | string[][]; | ||
| } else { | ||
| throw firstDNSError; | ||
| } | ||
| } | ||
| }; | ||
| } |
There was a problem hiding this comment.
Can we choose an approach that doesn't necessitate casting? One approach would be something like:
| return async function dnsReqRetryTimeout(lookupAddress: string) { | |
| try { | |
| return await dns.promises[api](lookupAddress); | |
| return (await dns.promises.resolve(lookupAddress, rrtype)) as dns.SrvRecord[] | string[][]; | |
| } catch (firstDNSError) { | |
| if (firstDNSError.code === dns.TIMEOUT) { | |
| return await dns.promises[api](lookupAddress); | |
| return (await dns.promises.resolve(lookupAddress, rrtype)) as dns.SrvRecord[] | string[][]; | |
| } else { | |
| throw firstDNSError; | |
| } | |
| } | |
| }; | |
| } | |
| const resolve = | |
| rrtype === 'SRV' | |
| ? (address: string) => dns.promises.resolve(address, 'SRV') | |
| : (address: string) => dns.promises.resolve(address, 'TXT'); | |
| return async function dnsReqRetryTimeout(lookupAddress: string) { | |
| try { | |
| return await resolve(lookupAddress); | |
| } catch (firstDNSError) { | |
| if (firstDNSError.code === dns.TIMEOUT) { | |
| return await resolve(lookupAddress); | |
| } else { | |
| throw firstDNSError; | |
| } | |
| } | |
| }; |
Another would be modifying the function to instead accept a lookup function, which it then calls.
There was a problem hiding this comment.
@PavelSafronov I have updated this block using generics (basically map rrtype to response). please let me know if this works
There was a problem hiding this comment.
There's still a cast, it's just been moved to a different place
There was a problem hiding this comment.
In my view it's essentially the same as you suggested, but with the different mechanism: generics vs. runtime if-else:
const resolve =
rrtype === 'SRV'
? (address: string) => dns.promises.resolve(address, 'SRV')
: (address: string) => dns.promises.resolve(address, 'TXT');
// is effectively the same as (from the Typescript point of view)
interface DNSLookupMap {
SRV: dns.SrvRecord[];
TXT: string[][];
}
const resolve = () => dns.promises.resolve(lookupAddress, rrtype) as Promise<DNSLookupMap[T]>;
In other words, I don't want to add runtime check for the compilation step (it just feels wrong, however I'm not strongly against it).
Would you agree? Or is it casting in general that you would like to avoid?
There was a problem hiding this comment.
Casting in general. I don't feel terribly strongly, but if we have the capability to write this in way that doesn't require casting, I guess I don't see why we wouldn't?
Runtime checks to have a perf cost (although in general I think an if-statement is effectively negligable). But regardless, this is a factory, so the perf cost is once per module instantiation per function.
There was a problem hiding this comment.
agree! switched implementation back (with the resolve being defined with ternary condition)
PavelSafronov
left a comment
There was a problem hiding this comment.
Changes look good.
There was a problem hiding this comment.
Pull request overview
Refactors the driver’s DNS resolution paths so SRV/TXT/CNAME/PTR lookups consistently route through dns.resolve(...) (and dns.lookup(...) where appropriate), aligning runtime behavior and test stubbing with the unified API.
Changes:
- Updated SRV polling to use
dns.promises.resolve(host, 'SRV')instead ofresolveSrv. - Updated SRV/TXT connection-string resolution retry logic to use
dns.promises.resolve(host, rrtype). - Updated GSSAPI canonicalization to use
dns.promises.resolve(host, 'PTR' | 'CNAME')and adjusted related unit/manual/integration tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/sdam/srv_polling.ts |
Switch SRV polling to dns.promises.resolve(..., 'SRV'). |
src/connection_string.ts |
Refactor DNS timeout retry helper to use `resolve(..., 'SRV' |
src/cmap/auth/gssapi.ts |
Replace resolvePtr/resolveCname with `resolve(..., 'PTR' |
test/unit/sdam/srv_polling.test.ts |
Update SRV polling DNS stubs/assertions for resolve. |
test/unit/connection_string.test.ts |
Update SRV/TXT stubbing to a single dns.promises.resolve stub with rrtype matching. |
test/unit/cmap/auth/gssapi.test.ts |
Update canonicalization tests to use dns.resolve(..., rrtype) spies/stubs. |
test/unit/assorted/polling_srv_records_for_mongos_discovery.prose.test.ts |
Update SRV polling prose test to stub dns.promises.resolve. |
test/manual/kerberos.test.ts |
Update kerberos DNS assertions/stubs to use dns.resolve(..., rrtype) and dns.lookup. |
test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts |
Update seedlist discovery prose test to stub dns.promises.resolve for SRV/TXT. |
test/integration/initial-dns-seedlist-discovery/dns_seedlist.test.ts |
Update DNS timeout tests to stub dns.promises.resolve with rrtype matching. |
Comments suppressed due to low confidence (1)
test/unit/assorted/polling_srv_records_for_mongos_discovery.prose.test.ts:298
sinon.stub(dns.promises, 'resolve')here intercepts allresolve()calls, including the driver's TXT lookup (resolve(host, 'TXT')) during SRV connection string processing. Since this stub always returns SRV records, TXT resolution will return the wrong shape and can breakresolveSRVRecord. Prefer scoping the stub withwithArgs(<hostname>, 'SRV')(and let'TXT'call through or reject with{ code: 'ENODATA' }).
// first call is for the driver initial connection
// second call will check the poller
resolveSrvStub = sinon.stub(dns.promises, 'resolve').callsFake(async address => {
expect(address).to.equal(`_${srvServiceName}._tcp.test.mock.test.build.10gen.cc`);
if (initialDNSLookup) {
initialDNSLookup = false;
return initialRecords;
}
return replacementRecords;
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| expect(dns.promises.resolveSrv).to.have.been.calledOnce.and.to.have.been.calledWith( | ||
| expect(dns.promises.resolve).to.have.been.calledOnce.and.to.have.been.calledWith( | ||
| `_mongodb._tcp.${SRV_HOST}` |
There was a problem hiding this comment.
SrvPoller._poll() now calls dns.promises.resolve(this.srvAddress, 'SRV'), but this test asserts the call only received the hostname. Update the expectation (and optionally the stub) to include the 'SRV' rrtype argument so the assertion matches the real invocation.
| `_mongodb._tcp.${SRV_HOST}` | |
| `_mongodb._tcp.${SRV_HOST}`, | |
| 'SRV' |
| expect(dns.lookup).to.be.calledOnceWith(hostName); | ||
| expect(dns.resolvePtr).to.be.calledOnceWith(lookedUp.address); | ||
| expect(dns.resolveCname).to.not.be.called; | ||
| expect(dns.resolve).to.be.calledOnceWith(lookedUp.address, 'PTR'); | ||
| expect(dns.resolve).to.not.be.calledOnceWith(lookedUp.address, 'CNAME'); | ||
| }); |
There was a problem hiding this comment.
This assertion uses to.not.be.calledOnceWith(...), which can still pass even if dns.resolve was called with the forbidden args multiple times (it only asserts the call count isn't exactly 1). To guarantee no CNAME lookup happened here, assert dns.resolve was not called with (lookedUp.address, 'CNAME') (e.g., not.be.calledWith) or explicitly assert the only call was the PTR one.
| expect(host).to.equal(resolved); | ||
| expect(dns.lookup).to.be.calledOnceWith(hostName); | ||
| expect(dns.resolvePtr).to.be.calledOnceWith(lookedUp.address); | ||
| expect(dns.resolveCname).to.not.be.called; | ||
| expect(dns.resolve).to.be.calledOnceWith(lookedUp.address, 'PTR'); | ||
| expect(dns.resolve).to.not.be.calledOnceWith(sinon.match.string, 'CNAME'); | ||
| }); |
There was a problem hiding this comment.
Same issue as above: to.not.be.calledOnceWith(...) does not prove CNAME was never queried, only that it wasn't queried exactly once with those args. Use not.be.calledWith(..., 'CNAME') (or assert the call set explicitly) to make the test robust.
| ); | ||
| await client.connect(); | ||
| expect(dns.resolveCname).to.be.calledOnceWith(host); | ||
| expect(resolveStub.withArgs(sinon.match.any, 'CNAME')).to.be.calledOnceWith(host); |
There was a problem hiding this comment.
resolveStub.withArgs(..., 'CNAME') matches calls like (host, 'CNAME'), so the assertion should include the rrtype argument (or assert on resolveStub directly). As written, calledOnceWith(host) will fail because the actual call includes a second 'CNAME' argument.
| expect(resolveStub.withArgs(sinon.match.any, 'CNAME')).to.be.calledOnceWith(host); | |
| expect(resolveStub.withArgs(sinon.match.any, 'CNAME')).to.be.calledOnceWith(host, 'CNAME'); |
| beforeEach(function () { | ||
| resolveCnameSpy.restore(); | ||
| sinon.stub(dns, 'resolveCname').resolves([]); | ||
| resolveStub.withArgs(sinon.match.string, 'CNAME').rejects([]); |
There was a problem hiding this comment.
In the "cname lookup is empty" setup, the stub uses rejects([]), but an empty CNAME result should be a resolved value (resolves([])), not a rejected promise. Using rejects([]) changes the code path to the error/fallback behavior and also rejects with a non-Error value.
| resolveStub.withArgs(sinon.match.string, 'CNAME').rejects([]); | |
| resolveStub.withArgs(sinon.match.string, 'CNAME').resolves([]); |
Description
Refactors all dns resolution to only go through
lookupandresolve.Summary of Changes
lookupandresolvesWhat is the motivation for this change?
NODE-7313
Release Highlight
Release notes highlight
Double check the following
npm run check:lint)type(NODE-xxxx)[!]: descriptionfeat(NODE-1234)!: rewriting everything in coffeescript