Commit c0f7610af0 for bind

commit c0f7610af04c73570464312e1d868f22f635ac66
Author: Štěpán Balážik <stepan@isc.org>
Date:   Tue Sep 8 19:33:57 2026 +0200

    Replace QnameHandler with a matcher

    QnameHandler existed only to match on a list of QNAMEs; its users now
    declare `Qname(...)` themselves.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/isctest/asyncserver/handlers.py b/bin/tests/system/isctest/asyncserver/handlers.py
index 797da2a90f..6acdd1964d 100644
--- a/bin/tests/system/isctest/asyncserver/handlers.py
+++ b/bin/tests/system/isctest/asyncserver/handlers.py
@@ -187,13 +187,12 @@ class IgnoreAllQueries(ResponseHandler):
         yield ResponseDrop()


-class QnameHandler(ResponseHandler):
+class QnameQtypeHandler(ResponseHandler):
     """
-    Base class used for deriving custom QNAME handlers.
+    Handle queries for which both of the following conditions are true:

-    The derived class must specify a list of `qnames` that it wants to handle.
-    Queries for exactly these QNAMEs will then be passed to the
-    `get_response()` method in the derived class.
+    - the query's QNAME is present in `self.qnames`,
+    - the query's QTYPE is present in `self.qtypes`.
     """

     @property
@@ -204,28 +203,6 @@ class QnameHandler(ResponseHandler):
         """
         raise NotImplementedError

-    def __init__(self) -> None:
-        self._qnames: list[dns.name.Name] = [dns.name.from_text(d) for d in self.qnames]
-
-    def __str__(self) -> str:
-        return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)})"
-
-    def match(self, qctx: QueryContext) -> bool:
-        """
-        Handle queries whose QNAME matches any of the QNAMEs handled by this
-        class.
-        """
-        return qctx.qname in self._qnames
-
-
-class QnameQtypeHandler(QnameHandler):
-    """
-    Handle queries for which both of the following conditions are true:
-
-    - the query's QNAME is present in `self.qnames`,
-    - the query's QTYPE is present in `self.qtypes`.
-    """
-
     @property
     @abc.abstractmethod
     def qtypes(self) -> list[dns.rdatatype.RdataType]:
@@ -235,7 +212,7 @@ class QnameQtypeHandler(QnameHandler):
         raise NotImplementedError

     def __init__(self) -> None:
-        super().__init__()
+        self._qnames: list[dns.name.Name] = [dns.name.from_text(d) for d in self.qnames]
         self._qtypes: list[dns.rdatatype.RdataType] = self.qtypes

     def __str__(self) -> str:
@@ -246,7 +223,7 @@ class QnameQtypeHandler(QnameHandler):
         Handle queries whose QNAME and QTYPE match any of the QNAMEs and
         QTYPEs handled by this class.
         """
-        return qctx.qtype in self._qtypes and super().match(qctx)
+        return qctx.qtype in self._qtypes and qctx.qname in self._qnames


 class _UnsetEdnsType:
diff --git a/bin/tests/system/isctest/asyncserver/matchers.py b/bin/tests/system/isctest/asyncserver/matchers.py
index 27ece151a7..60a8151f41 100644
--- a/bin/tests/system/isctest/asyncserver/matchers.py
+++ b/bin/tests/system/isctest/asyncserver/matchers.py
@@ -11,6 +11,8 @@

 import abc

+import dns.name
+
 from .context import QueryContext


@@ -106,3 +108,21 @@ class Always(Matcher):

     def match(self, qctx: QueryContext) -> bool:
         return True
+
+
+class Qname(Matcher):
+    """
+    Match queries whose QNAME is one of the given names.
+    """
+
+    def __init__(self, *qnames: str | dns.name.Name) -> None:
+        self.qnames = [
+            name if isinstance(name, dns.name.Name) else dns.name.from_text(name)
+            for name in qnames
+        ]
+
+    def match(self, qctx: QueryContext) -> bool:
+        return qctx.qname in self.qnames
+
+    def __str__(self) -> str:
+        return f"QNAME in [{', '.join(str(name) for name in self.qnames)}]"
diff --git a/bin/tests/system/qpcache_rrsig_any/ans3/ans.py b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py
index 8f47757163..c0a6e34153 100644
--- a/bin/tests/system/qpcache_rrsig_any/ans3/ans.py
+++ b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py
@@ -19,7 +19,8 @@ import dns.rrset

 from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import QnameHandler, StaticResponseHandler
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname


 def rrsig_covering(
@@ -35,7 +36,7 @@ def rrsig_covering(
     )


-class RrsigCoversRrsigHandler(QnameHandler, StaticResponseHandler):
+class RrsigCoversRrsigHandler(StaticResponseHandler):
     """
     An RRSIG covering RRSIG only trips the QP-cache RRSIG-pairing assertion when
     a second RRSIG header shares the owner name, so serve two ordinary RRSIGs
@@ -43,11 +44,11 @@ class RrsigCoversRrsigHandler(QnameHandler, StaticResponseHandler):
     RRSIG-covers-RRSIG record is cached harmlessly.
     """

-    qnames = ["rrsig.attacker.test."]
+    matcher = Qname("rrsig.attacker.test.")
     answer = [
-        rrsig_covering(qnames[0], dns.rdatatype.A),
-        rrsig_covering(qnames[0], dns.rdatatype.AAAA),
-        rrsig_covering(qnames[0], dns.rdatatype.RRSIG),
+        rrsig_covering(matcher.qnames[0], dns.rdatatype.A),
+        rrsig_covering(matcher.qnames[0], dns.rdatatype.AAAA),
+        rrsig_covering(matcher.qnames[0], dns.rdatatype.RRSIG),
     ]


diff --git a/bin/tests/system/reclimit/reclimit_ans.py b/bin/tests/system/reclimit/reclimit_ans.py
index 3c35d0b489..230a57b210 100644
--- a/bin/tests/system/reclimit/reclimit_ans.py
+++ b/bin/tests/system/reclimit/reclimit_ans.py
@@ -29,16 +29,16 @@ from isctest.asyncserver import (
     ResponseHandler,
 )
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import QnameHandler
+from isctest.asyncserver.matchers import Qname


-class ReclimitStateHandler(QnameHandler):
+class ReclimitStateHandler(ResponseHandler):
     """
     Handler for the "count." and "reset." queries that also holds the state
     shared by all the handlers in one server.
     """

-    qnames = ["count.", "reset."]
+    matcher = Qname("count.", "reset.")

     def __init__(self, indirect_send_response_default: bool = True) -> None:
         self._indirect_send_response_default = indirect_send_response_default
@@ -128,8 +128,8 @@ def ns(owner: str | dns.name.Name, target: str | dns.name.Name) -> dns.rrset.RRs
     )


-class DirectExampleHandler(ReclimitHandler, QnameHandler):
-    qnames = ["direct.example.org", "direct.example.net"]
+class DirectExampleHandler(ReclimitHandler):
+    matcher = Qname("direct.example.org", "direct.example.net")

     def __init__(
         self, state_handler: ReclimitStateHandler, local_ns_number: int
@@ -145,8 +145,8 @@ class DirectExampleHandler(ReclimitHandler, QnameHandler):
         yield DnsResponseSend(qctx.response)


-class IndirectExampleOrgHandler(ReclimitHandler, QnameHandler):
-    qnames = [f"indirect{i}.example.org" for i in range(1, 9)]
+class IndirectExampleOrgHandler(ReclimitHandler):
+    matcher = Qname(*[f"indirect{i}.example.org" for i in range(1, 9)])

     def __init__(
         self, state_handler: ReclimitStateHandler, local_ns_number: int
diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py
index 393ba43d99..8ace998757 100644
--- a/bin/tests/system/resend_loop/ans3/ans.py
+++ b/bin/tests/system/resend_loop/ans3/ans.py
@@ -22,10 +22,10 @@ from isctest.asyncserver import AsyncDnsServer, QueryContext
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import (
     DomainHandler,
-    QnameHandler,
     QnameQtypeHandler,
     StaticResponseHandler,
 )
+from isctest.asyncserver.matchers import Qname


 def rrset(
@@ -78,8 +78,8 @@ class ExampleCookieHandler(DomainHandler):
             yield DnsResponseSend(qctx.response)


-class TestDotComServFailHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["test.com."]
+class TestDotComServFailHandler(StaticResponseHandler):
+    matcher = Qname("test.com.")
     authoritative = False
     rcode = dns.rcode.SERVFAIL

diff --git a/bin/tests/system/resolver/ans2/ans.py b/bin/tests/system/resolver/ans2/ans.py
index 1c9aebe22a..1421340f61 100644
--- a/bin/tests/system/resolver/ans2/ans.py
+++ b/bin/tests/system/resolver/ans2/ans.py
@@ -22,10 +22,10 @@ from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import (
     DomainHandler,
     IgnoreAllQueries,
-    QnameHandler,
     QnameQtypeHandler,
     StaticResponseHandler,
 )
+from isctest.asyncserver.matchers import Qname

 from ..resolver_ans import (
     DelegationHandler,
@@ -58,28 +58,28 @@ def _cname_rrsets(
     )


-class Cname1Handler(QnameHandler, StaticResponseHandler):
-    qnames = ["cname1.example.com."]
+class Cname1Handler(StaticResponseHandler):
+    matcher = Qname("cname1.example.com.")
     # Data for the "cname + other data / 1" test
-    answer = _cname_rrsets(qnames[0])
+    answer = _cname_rrsets(matcher.qnames[0])
     authoritative = False


-class Cname2Handler(QnameHandler, StaticResponseHandler):
-    qnames = ["cname2.example.com."]
+class Cname2Handler(StaticResponseHandler):
+    matcher = Qname("cname2.example.com.")
     # Data for the "cname + other data / 2" test: same RRs in opposite order
-    answer = tuple(reversed(_cname_rrsets(qnames[0])))
+    answer = tuple(reversed(_cname_rrsets(matcher.qnames[0])))
     authoritative = False


-class ExampleOrgHandler(QnameHandler):
-    qnames = [
+class ExampleOrgHandler(ResponseHandler):
+    matcher = Qname(
         "www.example.org",
         "badcname.example.org",
         "goodcname.example.org",
         "foo.baddname.example.org",
         "foo.gooddname.example.org",
-    ]
+    )

     async def get_responses(
         self, qctx: QueryContext
@@ -94,8 +94,8 @@ class ExampleOrgHandler(QnameHandler):
         yield DnsResponseSend(qctx.response, authoritative=True)


-class NoResponseExampleUdpHandler(QnameHandler, IgnoreAllQueries):
-    qnames = ["noresponse.exampleudp.net."]
+class NoResponseExampleUdpHandler(IgnoreAllQueries):
+    matcher = Qname("noresponse.exampleudp.net.")


 class RootNsHandler(QnameQtypeHandler):
@@ -117,8 +117,8 @@ class RootNsHandler(QnameQtypeHandler):
         yield DnsResponseSend(qctx.response, authoritative=True)


-class ZoneVersionHandler(QnameHandler):
-    qnames = ["zoneversion."]
+class ZoneVersionHandler(ResponseHandler):
+    matcher = Qname("zoneversion.")

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/resolver/ans3/ans.py b/bin/tests/system/resolver/ans3/ans.py
index e4c92b1fa0..a819b6c1f6 100644
--- a/bin/tests/system/resolver/ans3/ans.py
+++ b/bin/tests/system/resolver/ans3/ans.py
@@ -20,10 +20,10 @@ from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import (
     DomainHandler,
     IgnoreAllQueries,
-    QnameHandler,
     QnameQtypeHandler,
     StaticResponseHandler,
 )
+from isctest.asyncserver.matchers import Qname

 from ..resolver_ans import (
     DelegationHandler,
@@ -44,14 +44,14 @@ class ApexNSHandler(QnameQtypeHandler, StaticResponseHandler):
     additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.3")]


-class AttackDnameHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["www.example.attack.example.net", "isc.attack.example.net."]
+class AttackDnameHandler(StaticResponseHandler):
+    matcher = Qname("www.example.attack.example.net", "isc.attack.example.net.")
     answer = [rrset("attack.example.net.", dns.rdatatype.DNAME, "org.")]


-class BadCnameHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["badcname.example.net."]
-    answer = [rrset(qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")]
+class BadCnameHandler(StaticResponseHandler):
+    matcher = Qname("badcname.example.net.")
+    answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")]


 class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler):
@@ -60,9 +60,9 @@ class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler):
     authority = [soa_rrset("example.net.")]


-class CnameSubHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["cname.sub.example.org."]
-    answer = [rrset(qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")]
+class CnameSubHandler(StaticResponseHandler):
+    matcher = Qname("cname.sub.example.org.")
+    answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")]


 class ExampleOrgHandler(QnameQtypeHandler, StaticResponseHandler):
@@ -71,33 +71,33 @@ class ExampleOrgHandler(QnameQtypeHandler, StaticResponseHandler):
     answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")]


-class FooBadDnameHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["foo.baddname.example.net."]
+class FooBadDnameHandler(StaticResponseHandler):
+    matcher = Qname("foo.baddname.example.net.")
     answer = [
         rrset("baddname.example.net.", dns.rdatatype.DNAME, "baddname.example.org.")
     ]


-class FooBarSubTld1Handler(QnameHandler, StaticResponseHandler):
-    qnames = ["foo.bar.sub.tld1."]
-    answer = [rrset(qnames[0], dns.rdatatype.TXT, "baz")]
+class FooBarSubTld1Handler(StaticResponseHandler):
+    matcher = Qname("foo.bar.sub.tld1.")
+    answer = [rrset(matcher.qnames[0], dns.rdatatype.TXT, "baz")]


-class FooGlueInAnswerHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["foo.glue-in-answer.example.org."]
-    answer = [rrset(qnames[0], dns.rdatatype.A, "192.0.2.1")]
+class FooGlueInAnswerHandler(StaticResponseHandler):
+    matcher = Qname("foo.glue-in-answer.example.org.")
+    answer = [rrset(matcher.qnames[0], dns.rdatatype.A, "192.0.2.1")]


-class FooGoodDnameHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["foo.gooddname.example.net."]
+class FooGoodDnameHandler(StaticResponseHandler):
+    matcher = Qname("foo.gooddname.example.net.")
     answer = [
         rrset("gooddname.example.net.", dns.rdatatype.DNAME, "gooddname.example.org.")
     ]


-class GoodCnameHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["goodcname.example.net."]
-    answer = [rrset(qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")]
+class GoodCnameHandler(StaticResponseHandler):
+    matcher = Qname("goodcname.example.net.")
+    answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")]


 class IscHandler(QnameQtypeHandler, StaticResponseHandler):
@@ -138,26 +138,26 @@ class LongCnameHandler(ResponseHandler):
         yield DnsResponseSend(qctx.response)


-class NodataHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["nodata.example.net."]
+class NodataHandler(StaticResponseHandler):
+    matcher = Qname("nodata.example.net.")


-class NoresponseHandler(QnameHandler, IgnoreAllQueries):
-    qnames = ["noresponse.example.net."]
+class NoresponseHandler(IgnoreAllQueries):
+    matcher = Qname("noresponse.example.net.")


-class NsHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["ns.example.net."]
-    answer = [rrset(qnames[0], dns.rdatatype.A, "10.53.0.3")]
+class NsHandler(StaticResponseHandler):
+    matcher = Qname("ns.example.net.")
+    answer = [rrset(matcher.qnames[0], dns.rdatatype.A, "10.53.0.3")]


-class NxdomainHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["nxdomain.example.net."]
+class NxdomainHandler(StaticResponseHandler):
+    matcher = Qname("nxdomain.example.net.")
     rcode = dns.rcode.NXDOMAIN


-class OkSubHandler(QnameHandler):
-    qnames = ["ok.sub.example.org.", "www.ok.sub.example.org."]
+class OkSubHandler(ResponseHandler):
+    matcher = Qname("ok.sub.example.org.", "www.ok.sub.example.org.")

     async def get_responses(
         self, qctx: QueryContext
@@ -178,20 +178,20 @@ class PartialFormerrHandler(DomainHandler):
         yield DnsResponseSend(qctx.response)


-class WwwDnameSubHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["www.dname.sub.example.org."]
+class WwwDnameSubHandler(StaticResponseHandler):
+    matcher = Qname("www.dname.sub.example.org.")
     answer = [
         rrset("dname.sub.example.org.", dns.rdatatype.DNAME, "ok.sub.example.org.")
     ]


-class WwwGoodDnameHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["www.example.gooddname.example.net"]
+class WwwGoodDnameHandler(StaticResponseHandler):
+    matcher = Qname("www.example.gooddname.example.net")
     answer = [rrset("gooddname.example.net.", dns.rdatatype.DNAME, "org.")]


-class WwwHandler(QnameHandler):
-    qnames = ["www.example.net."]
+class WwwHandler(ResponseHandler):
+    matcher = Qname("www.example.net.")

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/resolver/ans8/ans.py b/bin/tests/system/resolver/ans8/ans.py
index 446aa428ed..d11cfc7af6 100644
--- a/bin/tests/system/resolver/ans8/ans.py
+++ b/bin/tests/system/resolver/ans8/ans.py
@@ -15,6 +15,7 @@ import abc

 import dns.flags
 import dns.message
+import dns.name
 import dns.rcode
 import dns.rdatatype

@@ -27,10 +28,10 @@ from isctest.asyncserver import (
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import (
     DomainHandler,
-    QnameHandler,
     QnameQtypeHandler,
     StaticResponseHandler,
 )
+from isctest.asyncserver.matchers import Qname

 from ..resolver_ans import rrset

@@ -59,13 +60,14 @@ class HeaderOnlyHandler(ResponseHandler):
         yield DnsResponseSend(message, acknowledge_hand_rolled_response=True)


-class RefusedOnTcpHandler(QnameHandler, HeaderOnlyHandler):
-    qnames = ["tcpalso.no-questions."]
+class RefusedOnTcpHandler(HeaderOnlyHandler):
     flags = dns.flags.QR
     rcode = dns.rcode.REFUSED

     def match(self, qctx: QueryContext) -> bool:
-        return qctx.protocol == DnsProtocol.TCP and super().match(qctx)
+        return qctx.protocol == DnsProtocol.TCP and qctx.qname == dns.name.from_text(
+            "tcpalso.no-questions."
+        )


 class TcpFallbackHandler(ResponseHandler):
@@ -91,8 +93,8 @@ class NoQuestionsNSHandler(QnameQtypeHandler, StaticResponseHandler):
     additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.8")]


-class NsNoQuestionsAHandler(QnameHandler):
-    qnames = ["ns.no-questions."]
+class NsNoQuestionsAHandler(ResponseHandler):
+    matcher = Qname("ns.no-questions.")

     async def get_responses(
         self, qctx: QueryContext
@@ -103,14 +105,14 @@ class NsNoQuestionsAHandler(QnameHandler):
         yield DnsResponseSend(qctx.response)


-class TcpalsoNoQuestionsHandler(QnameHandler, HeaderOnlyHandler):
-    qnames = ["tcpalso.no-questions."]
+class TcpalsoNoQuestionsHandler(HeaderOnlyHandler):
+    matcher = Qname("tcpalso.no-questions.")
     flags = dns.flags.QR | dns.flags.TC
     rcode = dns.rcode.REFUSED


-class TruncatedNoQuestionsHandler(QnameHandler, HeaderOnlyHandler):
-    qnames = ["truncated.no-questions."]
+class TruncatedNoQuestionsHandler(HeaderOnlyHandler):
+    matcher = Qname("truncated.no-questions.")
     flags = dns.flags.QR | dns.flags.AA | dns.flags.TC


diff --git a/bin/tests/system/resolver/resolver_ans.py b/bin/tests/system/resolver/resolver_ans.py
index d81a67e0d6..cd1d3b1d6a 100644
--- a/bin/tests/system/resolver/resolver_ans.py
+++ b/bin/tests/system/resolver/resolver_ans.py
@@ -19,9 +19,10 @@ import dns.rdataclass
 import dns.rdatatype
 import dns.rrset

-from isctest.asyncserver import QueryContext
+from isctest.asyncserver import QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import DomainHandler, QnameHandler
+from isctest.asyncserver.handlers import DomainHandler
+from isctest.asyncserver.matchers import Qname


 def rrset(
@@ -84,8 +85,8 @@ class DelegationHandler(DomainHandler):
         yield DnsResponseSend(qctx.response, authoritative=False)


-class Gl6412AHandler(QnameHandler):
-    qnames = ["a.gl6412.", "a.a.gl6412."]
+class Gl6412AHandler(ResponseHandler):
+    matcher = Qname("a.gl6412.", "a.a.gl6412.")

     async def get_responses(
         self, qctx: QueryContext
@@ -94,8 +95,8 @@ class Gl6412AHandler(QnameHandler):
         yield DnsResponseSend(qctx.response)


-class Gl6412Handler(QnameHandler):
-    qnames = ["gl6412."]
+class Gl6412Handler(ResponseHandler):
+    matcher = Qname("gl6412.")

     async def get_responses(
         self, qctx: QueryContext
@@ -112,8 +113,8 @@ class Gl6412Handler(QnameHandler):
         yield DnsResponseSend(qctx.response)


-class Gl6412Ns2Handler(QnameHandler):
-    qnames = ["ns2.gl6412."]
+class Gl6412Ns2Handler(ResponseHandler):
+    matcher = Qname("ns2.gl6412.")

     async def get_responses(
         self, qctx: QueryContext
@@ -126,8 +127,8 @@ class Gl6412Ns2Handler(QnameHandler):
         yield DnsResponseSend(qctx.response)


-class Gl6412Ns3Handler(QnameHandler):
-    qnames = ["ns3.gl6412."]
+class Gl6412Ns3Handler(ResponseHandler):
+    matcher = Qname("ns3.gl6412.")

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/serve_stale/ans2/ans.py b/bin/tests/system/serve_stale/ans2/ans.py
index add5b14d9e..417fb9944d 100644
--- a/bin/tests/system/serve_stale/ans2/ans.py
+++ b/bin/tests/system/serve_stale/ans2/ans.py
@@ -14,11 +14,8 @@ import dns.rdatatype

 from isctest.asyncserver import ControllableAsyncDnsServer, ResponseHandler
 from isctest.asyncserver.commands import ToggleResponsesCommand
-from isctest.asyncserver.handlers import (
-    QnameHandler,
-    QnameQtypeHandler,
-    StaticResponseHandler,
-)
+from isctest.asyncserver.handlers import QnameQtypeHandler, StaticResponseHandler
+from isctest.asyncserver.matchers import Qname

 from ..serve_stale_ans import (
     a_handler,
@@ -34,8 +31,8 @@ from ..serve_stale_ans import (
 )


-class NxdomainExampleHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["nxdomain.example."]
+class NxdomainExampleHandler(StaticResponseHandler):
+    matcher = Qname("nxdomain.example.")
     rcode = dns.rcode.NXDOMAIN
     authority = [soa("example.", ttl=2)]

@@ -43,13 +40,13 @@ class NxdomainExampleHandler(QnameHandler, StaticResponseHandler):
 # A negative answer that stays fresh for the whole run of a test, so that a
 # resolver refreshing it can only be doing so because it wrongly considers
 # the cached entry stale.
-class LongttlNodataExampleHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["longttl-nodata.example."]
+class LongttlNodataExampleHandler(StaticResponseHandler):
+    matcher = Qname("longttl-nodata.example.")
     authority = [soa("example.", ttl=600, minimum=600)]


-class LongttlNxdomainExampleHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["longttl-nxdomain.example."]
+class LongttlNxdomainExampleHandler(StaticResponseHandler):
+    matcher = Qname("longttl-nxdomain.example.")
     rcode = dns.rcode.NXDOMAIN
     authority = [soa("example.", ttl=600, minimum=600)]

@@ -67,8 +64,8 @@ class OthertypeExampleCaaHandler(QnameQtypeHandler, StaticResponseHandler):
     ]


-class SourceStaleFallbackHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["source.stale."]
+class SourceStaleFallbackHandler(StaticResponseHandler):
+    matcher = Qname("source.stale.")


 def handlers() -> list[ResponseHandler]:
diff --git a/bin/tests/system/serve_stale/ans8/ans.py b/bin/tests/system/serve_stale/ans8/ans.py
index 1573dee6d6..3307160389 100644
--- a/bin/tests/system/serve_stale/ans8/ans.py
+++ b/bin/tests/system/serve_stale/ans8/ans.py
@@ -13,7 +13,8 @@ import dns.rcode

 from isctest.asyncserver import ControllableAsyncDnsServer, ResponseHandler
 from isctest.asyncserver.commands import SwitchControlCommand
-from isctest.asyncserver.handlers import QnameHandler, StaticResponseHandler
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname

 from ..serve_stale_ans import (
     a_handler,
@@ -26,8 +27,8 @@ from ..serve_stale_ans import (
 ANS8_ADDR = "10.53.0.8"


-class TargetStaleFallbackHandler(QnameHandler, StaticResponseHandler):
-    qnames = ["target.stale."]
+class TargetStaleFallbackHandler(StaticResponseHandler):
+    matcher = Qname("target.stale.")


 def handlers(www_address: str) -> list[ResponseHandler]:
diff --git a/bin/tests/system/statistics/ans4/ans.py b/bin/tests/system/statistics/ans4/ans.py
index 37fea5fcd5..a979d83e23 100644
--- a/bin/tests/system/statistics/ans4/ans.py
+++ b/bin/tests/system/statistics/ans4/ans.py
@@ -17,11 +17,12 @@ import dns.rrset

 from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import IgnoreAllQueries, QnameHandler
+from isctest.asyncserver.handlers import IgnoreAllQueries
+from isctest.asyncserver.matchers import Qname


-class FooInfoHandler(QnameHandler, IgnoreAllQueries):
-    qnames = ["foo.info."]
+class FooInfoHandler(IgnoreAllQueries):
+    matcher = Qname("foo.info.")


 class FallbackHandler(ResponseHandler):