diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,21 @@
 # Revision history for http3
 
+## 0.1.7
+
+* Requiring quic v0.3.10, which hands the application a stream only once
+  the frame that opened it has been checked.  A server used to answer on
+  a stream whose first frame was past the flow control limit, just before
+  the connection was closed over it.
+* Closing the connection with H3_CLOSED_CRITICAL_STREAM when a write on
+  one of our QPACK streams finds it closed, which the peer's STOP_SENDING
+  does.  It used to throw `StreamIsClosed` into whichever thread wrote.
+* QPACK: sending Set Dynamic Table Capacity only when the capacity
+  changes.  `getMaxTableCapacity` is new.
+* h3-server: allowing more unidirectional streams than the three a client
+  needs.
+* Tests: `h3ErrorSpec` takes how to read the number of requests served,
+  so that h3spec can run it against a server elsewhere.
+
 ## 0.1.6
 
 * Requiring quic v0.3.9 and http2 v5.4.6.  quic v0.3.7 stops opening a
diff --git a/Network/HTTP3/Control.hs b/Network/HTTP3/Control.hs
--- a/Network/HTTP3/Control.hs
+++ b/Network/HTTP3/Control.hs
@@ -7,6 +7,7 @@
 ) where
 
 import Control.Concurrent.MVar
+import qualified Control.Exception as E
 import qualified Data.ByteString as BS
 import Data.IORef
 import Data.IntSet (IntSet)
@@ -52,8 +53,21 @@
     -- decoder stream, and an instruction must not be split by another one.
     -- quic sends a write in two pieces when flow control stops it partway.
     lockD <- newMVar ()
-    return (sendStream sE, \bs -> withMVar lockD $ \_ -> sendStream sD bs)
+    return
+        ( critical $ sendStream sE
+        , critical $ \bs -> withMVar lockD $ \_ -> sendStream sD bs
+        )
   where
+    -- The sending part of one of our QPACK streams is closed only when the
+    -- peer asked for that with STOP_SENDING, which it must not do (RFC
+    -- 9204, section 4.2; RFC 9114, section 6.2.1): the closure of a
+    -- critical stream is a connection error of type H3_CLOSED_CRITICAL_STREAM.
+    -- The write used to throw StreamIsClosed into whichever thread made it,
+    -- and nothing caught it there.
+    critical send bs =
+        send bs `E.catch` \e -> case e of
+            StreamIsClosed -> abortConnection conn H3ClosedCriticalStream ""
+            _ -> E.throwIO e
     stC = mkType H3ControlStreams
     stE = mkType QPACKEncoderStream
     stD = mkType QPACKDecoderStream
diff --git a/Network/QPACK.hs b/Network/QPACK.hs
--- a/Network/QPACK.hs
+++ b/Network/QPACK.hs
@@ -174,9 +174,14 @@
                     -- MaxEntries.
                     setMaxEntries dyntbl n
                     let tableSize = min ecMaxTableCapacity n
-                    setTableCapacity dyntbl tableSize
-                    ins <- encodeEncoderInstructions [SetDynamicTableCapacity tableSize] False
-                    sendIns dyntbl ins
+                    current <- getMaxTableCapacity dyntbl
+                    -- The capacity starts at zero (RFC 9204, section 3.2.3),
+                    -- so an encoder that will not use the table has nothing
+                    -- to tell the decoder.
+                    when (tableSize /= current) $ do
+                        setTableCapacity dyntbl tableSize
+                        ins <- encodeEncoderInstructions [SetDynamicTableCapacity tableSize] False
+                        sendIns dyntbl ins
                 , setBlockedStreams = setMaxBlockedStreams dyntbl
                 , setHeaderSize = setMaxHeaderSize dyntbl
                 , getHeaderSize = getMaxHeaderSize dyntbl
diff --git a/Network/QPACK/Table/Dynamic.hs b/Network/QPACK/Table/Dynamic.hs
--- a/Network/QPACK/Table/Dynamic.hs
+++ b/Network/QPACK/Table/Dynamic.hs
@@ -10,6 +10,7 @@
     -- * Capacity
     isTableReady,
     getTableCapacity,
+    getMaxTableCapacity,
     setTableCapacity,
     setDecoderTableCapacity,
     getMaxNumOfEntries,
@@ -233,6 +234,11 @@
 
 getTableCapacity :: DynamicTable -> IO Int
 getTableCapacity DynamicTable{..} = readTVarIO tableSize
+
+-- | The capacity, as last set.  'getTableCapacity' is how much of it the
+--   entries take, whatever its name says.
+getMaxTableCapacity :: DynamicTable -> IO Int
+getMaxTableCapacity DynamicTable{..} = readIORef maxTableSize
 
 setTableCapacity :: DynamicTable -> Int -> IO ()
 setTableCapacity dyntbl@DynamicTable{..} maxsiz = do
diff --git a/http3.cabal b/http3.cabal
--- a/http3.cabal
+++ b/http3.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.4
 name:               http3
-version:            0.1.6
+version:            0.1.7
 license:            BSD-3-Clause
 license-file:       LICENSE
 maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>
@@ -89,7 +89,7 @@
         network-byte-order,
         network-control >= 0.1.7 && <0.2,
         psqueues,
-        quic >= 0.3.9 && < 0.4,
+        quic >= 0.3.10 && < 0.4,
         sockaddr,
         stm,
         time-manager >= 0.2.3 && <0.4,
diff --git a/test/HTTP3/Error.hs b/test/HTTP3/Error.hs
--- a/test/HTTP3/Error.hs
+++ b/test/HTTP3/Error.hs
@@ -16,6 +16,7 @@
 import Network.HTTP.Types
 import qualified Network.HTTP3.Client as H3
 import Network.HTTP3.Internal
+import Network.QPACK (QDecoderConfig (..), QEncoderConfig (..))
 import Network.QPACK.Internal
 import Network.QUIC
 import Network.QUIC.Client
@@ -71,12 +72,20 @@
         threadDelay 100000
         return ret
 
+-- | The error cases for an HTTP/3 server.
+--
+-- These also make h3spec, which runs them against a server somewhere else.
+-- The last argument says how, from what the spec is run with, to read how
+-- many requests the server has handed its application.  Only a server in the
+-- same process can say; given 'Nothing', a test does not check that a
+-- request the server refused never got that far.
 h3ErrorSpec
     :: ClientConfig
     -> H3.ClientConfig
     -> Millisecond
-    -> SpecWith (ThreadId, IORef Int)
-h3ErrorSpec qcc cconf ms = do
+    -> (a -> Maybe (IO Int))
+    -> SpecWith a
+h3ErrorSpec qcc cconf ms served = do
     conf0 <- runIO H3.allocSimpleConfig
     describe "HTTP/3 servers" $ do
         it
@@ -92,16 +101,12 @@
                 `shouldThrow` applicationProtocolErrorsIn [H3MessageError]
         it
             "MUST send H3_MESSAGE_ERROR if mandatory pseudo-header fields are absent [HTTP/3 4.1.3]"
-            $ \(_, served) -> do
+            $ \x -> do
                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader0
                     qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")
-                before' <- readIORef served
-                runC qcc' cconf conf ms
-                    `shouldThrow` applicationProtocolErrorsIn [H3MessageError]
-                -- And it must not have reached the application: the stream was
-                -- reset and then the request handed over anyway.
-                threadDelay 200000
-                readIORef served `shouldReturn` before'
+                refusedNotServed (served x) $
+                    runC qcc' cconf conf ms
+                        `shouldThrow` applicationProtocolErrorsIn [H3MessageError]
         it
             "MUST send H3_MESSAGE_ERROR if prohibited pseudo-header fields are present[HTTP/3 4.1.3]"
             $ \_ -> do
@@ -132,14 +137,12 @@
                     ++ C8.unpack (CI.original name)
                     ++ " as malformed [HTTP/3 4.2]"
                 )
-                $ \(_, served) -> do
+                $ \x -> do
                     let req = H3.requestNoBody methodGet "/" [field]
                         qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")
-                    before' <- readIORef served
-                    runCReq req qcc' cconf conf0 ms
-                        `shouldThrow` applicationProtocolErrorsIn [H3MessageError]
-                    threadDelay 200000
-                    readIORef served `shouldReturn` before'
+                    refusedNotServed (served x) $
+                        runCReq req qcc' cconf conf0 ms
+                            `shouldThrow` applicationProtocolErrorsIn [H3MessageError]
         it
             "MUST treat a request whose trailers carry connection as malformed [HTTP/3 4.2]"
             $ \_ -> do
@@ -245,45 +248,47 @@
             "MUST send H3_STREAM_CREATION_ERROR if a second control stream is opened [HTTP/3 6.2.1]"
             $ \_ -> do
                 -- A stream type of 0x00, then an empty SETTINGS frame.
-                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother "\x00\x04\x00"
-                runC qcc cconf conf ms
-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]
+                opened <- newIORef False
+                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother opened "\x00\x04\x00"
+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]
         it
             "MUST send H3_STREAM_CREATION_ERROR if a client opens a push stream [HTTP/3 6.2.2]"
             $ \_ -> do
                 -- A stream type of 0x01, then push ID 0.
-                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother "\x01\x00"
-                runC qcc cconf conf ms
-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]
+                opened <- newIORef False
+                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother opened "\x01\x00"
+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]
         it
             "MUST send H3_STREAM_CREATION_ERROR if a second encoder stream is opened [QPACK 4.2]"
             $ \_ -> do
-                let conf = addHook conf0 $ setOnEncoderStreamCreated $ openAnother "\x02"
-                runC qcc cconf conf ms
-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]
+                opened <- newIORef False
+                let conf = addHook conf0 $ setOnEncoderStreamCreated $ openAnother opened "\x02"
+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]
         it
             "MUST send H3_STREAM_CREATION_ERROR if a second decoder stream is opened [QPACK 4.2]"
             $ \_ -> do
-                let conf = addHook conf0 $ setOnDecoderStreamCreated $ openAnother "\x03"
-                runC qcc cconf conf ms
-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]
+                opened <- newIORef False
+                let conf = addHook conf0 $ setOnDecoderStreamCreated $ openAnother opened "\x03"
+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]
         it
             "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream is closed [QPACK 4.2]"
             $ \_ -> do
-                let conf = addHook conf0 $ setOnEncoderStreamCreated closeStream
+                let conf =
+                        noEncoderTable $ addHook conf0 $ setOnEncoderStreamCreated closeStream
                 runC qcc cconf conf ms
                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]
         it
             "MUST send H3_CLOSED_CRITICAL_STREAM if a decoder stream is closed [QPACK 4.2]"
             $ \_ -> do
-                let conf = addHook conf0 $ setOnDecoderStreamCreated closeStream
+                let conf =
+                        noDecoderTable $ addHook conf0 $ setOnDecoderStreamCreated closeStream
                 runC qcc cconf conf ms
                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]
         it
             "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream ends inside an instruction [QPACK 4.2]"
             $ \_ -> do
                 -- The first octet of a Set Dynamic Table Capacity that goes on.
-                let conf = addHook conf0 $ setOnEncoderStreamCreated $ \strm -> do
+                let conf = noEncoderTable $ addHook conf0 $ setOnEncoderStreamCreated $ \strm -> do
                         sendStream strm "\x3f"
                         closeStream strm
                 runC qcc cconf conf ms
@@ -297,6 +302,39 @@
 
 ----------------------------------------------------------------
 
+-- | Making a request the server must refuse and, where we can see what the
+-- server hands its application, checking that it handed over nothing: a
+-- stream used to be reset and the request handed over anyway.
+refusedNotServed :: Maybe (IO Int) -> IO () -> IO ()
+refusedNotServed Nothing refused = refused
+refusedNotServed (Just count) refused = do
+    before' <- count
+    refused
+    threadDelay 200000
+    count `shouldReturn` before'
+
+-- | A client whose encoder will not use the dynamic table, and so has
+-- nothing to write on its encoder stream.  For the tests that close that
+-- stream: a client that went on writing on it would see the closure itself
+-- and close the connection, and the test is about what the server does.
+noEncoderTable :: H3.Config -> H3.Config
+noEncoderTable conf =
+    conf
+        { H3.confQEncoderConfig =
+            (H3.confQEncoderConfig conf){ecMaxTableCapacity = 0}
+        }
+
+-- | A client whose decoder offers no dynamic table, and so has nothing to
+-- write on its decoder stream: no acknowledgements, since the server cannot
+-- refer to a table, and no Stream Cancellations, which are not sent then.
+-- For the test that closes that stream, as 'noEncoderTable'.
+noDecoderTable :: H3.Config -> H3.Config
+noDecoderTable conf =
+    conf
+        { H3.confQDecoderConfig =
+            (H3.confQDecoderConfig conf){dcMaxTableCapacity = 0}
+        }
+
 addHook :: H3.Config -> (H3.Hooks -> H3.Hooks) -> H3.Config
 addHook conf modify = conf'
   where
@@ -477,10 +515,36 @@
 
 -- | Opening a unidirectional stream of our own next to the one given, and
 -- sending it these octets: a stream type and whatever follows it.
-openAnother :: BS.ByteString -> Stream -> IO ()
-openAnother bs strm = do
-    strm' <- unidirectionalStream $ streamConnection strm
-    sendStream strm' bs
+--
+-- A client needs three unidirectional streams, and a server may let it have
+-- no more than that: quic's default does.  Then the stream cannot be opened
+-- at all, and this gives up after a tenth of a second rather than waiting
+-- for room that will not come, saying so in the 'IORef'.
+openAnother :: IORef Bool -> BS.ByteString -> Stream -> IO ()
+openAnother opened bs strm = do
+    mstrm <- timeout 100000 $ unidirectionalStream $ streamConnection strm
+    forM_ mstrm $ \strm' -> do
+        sendStream strm' bs
+        writeIORef opened True
+
+-- | Expecting one of the errors when 'openAnother' could open its stream,
+-- and leaving the test pending when it could not: against a server that
+-- lets a client have only the streams it needs, a second control or QPACK
+-- stream cannot be opened, and there is nothing to check.
+expectIfOpened
+    :: IORef Bool -> IO (Maybe ()) -> [ApplicationProtocolError] -> IO ()
+expectIfOpened opened action errs = do
+    r <- E.try action
+    ok <- readIORef opened
+    if not ok
+        then
+            pendingWith
+                "the server allows no unidirectional stream beyond the three a client needs"
+        else case r of
+            Left e
+                | applicationProtocolErrorsIn errs e -> return ()
+                | otherwise -> expectationFailure $ "unexpected exception: " ++ show e
+            Right _ -> expectationFailure "did not get expected exception"
 
 -- A GOAWAY frame announcing 2^30 octets and then sending none of them.
 --
diff --git a/test/HTTP3/ErrorSpec.hs b/test/HTTP3/ErrorSpec.hs
--- a/test/HTTP3/ErrorSpec.hs
+++ b/test/HTTP3/ErrorSpec.hs
@@ -14,8 +14,9 @@
 spec =
     beforeAll start $
         afterAll (teardown . fst) $
-            h3ErrorSpec testClientConfig testH3ClientConfig 2000 -- 2 seconds
+            h3ErrorSpec testClientConfig testH3ClientConfig 2000 served -- 2 seconds
   where
+    served (_, ref) = Just $ readIORef ref
     start = do
         ref <- newIORef 0
         tid <- setup (countingServer ref) 4096
diff --git a/util/h3-server.hs b/util/h3-server.hs
--- a/util/h3-server.hs
+++ b/util/h3-server.hs
@@ -138,6 +138,14 @@
                 , scGroups = getGroups (scGroups sc0) optGroups
                 , scQLog = optQLogDir
                 , scCredentials = Credentials [cred]
+                , -- Room for unidirectional streams beyond the three an HTTP/3
+                  -- client needs.  quic's default is exactly three, and then
+                  -- h3spec cannot open the second control or QPACK stream it
+                  -- needs to see this server refuse one; those cases were
+                  -- left pending.  RFC 9114, section 6.2, suggests allowing
+                  -- more anyway, for extensions and reserved stream types.
+                  scParameters =
+                    (scParameters sc0){initialMaxStreamsUni = 10}
                 }
     run sc $ \conn -> do
         info <- getConnectionInfo conn
