You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by da...@apache.org on 2026/09/20 01:12:14 UTC

(qpid-broker-j) 01/05: NO-JIRA: [Broker-J] AMQP 0-8 / 0-9 / 0-9-1 validation improvements

This is an automated email from the ASF dual-hosted git repository.

dakirily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git

commit 0db16e4115b0c922926dbd461216508cd030bbde
Author: Daniil Kirilyuk <da...@apache.org>
AuthorDate: Sat Sep 19 19:37:22 2026 +0200

    NO-JIRA: [Broker-J] AMQP 0-8 / 0-9 / 0-9-1 validation improvements
---
 .../apache/qpid/server/protocol/ErrorCodes.java    |    5 +
 .../NonBlockingConnectionExceptionTest.java        |  121 +++
 .../qpid/server/protocol/v0_8/AMQChannel.java      |  146 ++-
 .../qpid/server/protocol/v0_8/AMQDecoder.java      |   66 +-
 ...on.java => AMQFatalFrameDecodingException.java} |   27 +-
 .../protocol/v0_8/AMQFrameDecodingException.java   |   57 +-
 .../server/protocol/v0_8/AMQPConnection_0_8.java   |   11 +
 .../protocol/v0_8/AMQPConnection_0_8Impl.java      |  174 ++-
 .../apache/qpid/server/protocol/v0_8/AMQType.java  |   47 +-
 .../qpid/server/protocol/v0_8/AMQTypedValue.java   |   21 +-
 ...xception.java => AMQValueNestingException.java} |   26 +-
 .../qpid/server/protocol/v0_8/BrokerDecoder.java   |    9 +-
 .../qpid/server/protocol/v0_8/EncodingUtils.java   |   65 +-
 .../qpid/server/protocol/v0_8/FieldArray.java      |   13 +-
 .../qpid/server/protocol/v0_8/FieldTable.java      |  260 ++++-
 .../server/protocol/v0_8/FieldTableFactory.java    |    7 +
 .../protocol/v0_8/FieldValueNestingValidator.java  |  154 +++
 ...ameBoundaryValidatingServerMethodProcessor.java |  610 +++++++++++
 .../qpid/server/protocol/v0_8/IncomingMessage.java |   50 +-
 .../qpid/server/protocol/v0_8/ServerDecoder.java   |  501 +++++----
 .../protocol/v0_8/transport/BasicConsumeBody.java  |   32 +-
 .../transport/BasicContentHeaderProperties.java    |   58 +-
 .../protocol/v0_8/transport/ChannelAlertBody.java  |   14 +-
 .../v0_8/transport/ConnectionStartBody.java        |   25 +-
 .../v0_8/transport/ConnectionStartOkBody.java      |   35 +-
 .../protocol/v0_8/transport/ContentHeaderBody.java |   94 +-
 .../v0_8/transport/ExchangeDeclareBody.java        |   33 +-
 .../protocol/v0_8/transport/HeartbeatBody.java     |   30 +-
 .../protocol/v0_8/transport/MethodProcessor.java   |    8 +
 .../protocol/v0_8/transport/QueueBindBody.java     |   26 +-
 .../protocol/v0_8/transport/QueueDeclareBody.java  |   26 +-
 .../protocol/v0_8/transport/QueueUnbindBody.java   |   26 +-
 .../transport/ServerChannelMethodProcessor.java    |   10 +
 .../qpid/server/protocol/v0_8/AMQChannelTest.java  |  192 ++++
 .../protocol/v0_8/AMQPConnection_0_8Test.java      |  985 ++++++++++++++++-
 .../server/protocol/v0_8/EncodingUtilsTest.java    |  192 +++-
 .../v0_8/FieldTableNestingDecoderTest.java         |  354 +++++++
 .../qpid/server/protocol/v0_8/FieldTableTest.java  |  369 ++++++-
 .../server/protocol/v0_8/IncomingMessageTest.java  |  125 +++
 .../server/protocol/v0_8/ServerDecoderTest.java    | 1103 ++++++++++++++++++++
 .../BasicContentHeaderPropertiesDisposalTest.java  |  460 ++++++++
 .../BasicContentHeaderPropertiesTest.java          |   39 +-
 .../ClientFieldTableBodyDisposalTest.java          |  106 ++
 .../v0_8/transport/ConnectionStartOkBodyTest.java  |  126 +++
 .../ServerFieldTableBodyDisposalTest.java          |  160 +++
 .../qpid/tests/protocol/v0_8/ConnectionTest.java   |  135 ++-
 .../protocol/v0_8/ContentBodyFrameLimitTest.java   |  108 ++
 .../v0_8/IncompleteContentSequenceTest.java        |  186 ++++
 .../tests/protocol/v0_8/MalformedFrameTest.java    |  244 +++++
 .../v0_8/extension/basic/MalformedMessageTest.java |   38 +-
 50 files changed, 7114 insertions(+), 595 deletions(-)

diff --git a/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java b/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java
index 88616c1593..cc729d0c54 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java
@@ -105,6 +105,11 @@ public interface ErrorCodes
      */
     int CHANNEL_ERROR = 504;
 
+    /**
+     * The peer sent a frame that was not expected, usually because content frames were sent out of sequence.
+     */
+    int UNEXPECTED_FRAME = 505;
+
     /**
      * The server could not complete the method because it lacked sufficient resources. This may be due to the client
      * creating too many of some type of entity.
diff --git a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
new file mode 100644
index 0000000000..3532a38769
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
@@ -0,0 +1,121 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+package org.apache.qpid.server.transport;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Collections;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionExceptionTest extends UnitTestBase
+{
+    private AmqpPort<?> _port;
+    private NetworkConnectionScheduler _scheduler;
+
+    @BeforeEach
+    void setUp()
+    {
+        final Broker<?> broker = mock(Broker.class);
+        when(broker.getEventLogger()).thenReturn(mock(EventLogger.class));
+        _port = mock(AmqpPort.class);
+        doReturn(broker).when(_port).getParent();
+        when(_port.getNetworkBufferSize()).thenReturn(128);
+        when(_port.getContextValue(Integer.class, AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(100);
+        when(_port.getContextValue(Long.class, AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(100L);
+        when(_port.getContextValue(Integer.class, AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
+        _scheduler = mock(NetworkConnectionScheduler.class);
+    }
+
+    @Test
+    void testContainedParsingFailureClosesOnlyItsConnection() throws Exception
+    {
+        final ProtocolEngine failingEngine = mock(ProtocolEngine.class);
+        final ProtocolEngine healthyEngine = mock(ProtocolEngine.class);
+        final SocketChannel failingChannel = mock(SocketChannel.class);
+        final SocketChannel healthyChannel = mock(SocketChannel.class);
+        final NonBlockingConnection failing = createConnection(failingEngine, failingChannel);
+        final NonBlockingConnection healthy = createConnection(healthyEngine, healthyChannel);
+        final ArithmeticException failure = new ArithmeticException("Injected decoding failure");
+        doThrow(new ConnectionScopedRuntimeException(failure)).when(failingEngine).received(any(QpidByteBuffer.class));
+
+        try
+        {
+            assertTrue(failing.doWork());
+            verify(failingEngine).closed();
+            verify(failingChannel).close();
+            verify(_scheduler).removeConnection(failing);
+
+            assertFalse(healthy.doWork());
+            verify(healthyEngine).received(any(QpidByteBuffer.class));
+        }
+        finally
+        {
+            failing.close();
+            failing.doWork();
+            healthy.close();
+            healthy.doWork();
+        }
+    }
+
+    private NonBlockingConnection createConnection(final ProtocolEngine engine, final SocketChannel channel)
+            throws Exception
+    {
+        final Socket socket = mock(Socket.class);
+        when(socket.getRemoteSocketAddress()).thenReturn(new InetSocketAddress("localhost", 1000));
+        when(socket.getLocalSocketAddress()).thenReturn(new InetSocketAddress("localhost", 5672));
+        when(channel.socket()).thenReturn(socket);
+        when(channel.read(any(ByteBuffer.class))).thenAnswer(invocation ->
+        {
+            final ByteBuffer input = invocation.getArgument(0);
+            input.put((byte) 0);
+            return 1;
+        });
+        when(engine.getAggregateTicker()).thenReturn(new AggregateTicker());
+        when(engine.processPendingIterator()).thenAnswer(invocation -> Collections.emptyIterator());
+        final NonBlockingConnection connection = new NonBlockingConnection(channel, engine,
+                Set.of(TransportEncryption.NONE), mock(Runnable.class), _scheduler, _port);
+        connection.setSelectionTask(mock(SelectorThread.SelectionTask.class));
+        return connection;
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java
index 0b2fba51e9..b76ba29e75 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java
@@ -155,6 +155,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
     private final CachedFrame _txCommitOkFrame;
 
     private final long _blockingTimeout;
+    private final int _maxContentBodyFramesPerMessage;
 
     /**
      * The delivery tag is unique per channel. This is pre-incremented before putting into the deliver frame so that
@@ -222,6 +223,11 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
 
         _forceMessageValidation = connection.getContextValue(Boolean.class, AMQPConnection_0_8.FORCE_MESSAGE_VALIDATION);
 
+        final Integer maxContentBodyFramesPerMessage = connection.getContextValue(
+                Integer.class, AMQPConnection_0_8.CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE);
+        _maxContentBodyFramesPerMessage = Math.max(1, maxContentBodyFramesPerMessage == null
+                ? AMQPConnection_0_8.DEFAULT_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE
+                : maxContentBodyFramesPerMessage);
     }
 
     private void message(final LogMessage message)
@@ -288,6 +294,41 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
         _currentMessage.setMessageDestination(e);
     }
 
+    private void rejectIncompleteMessage(final String message)
+    {
+        final int errorCode = ProtocolVersion.v0_91.equals(_connection.getProtocolVersion())
+                ? ErrorCodes.UNEXPECTED_FRAME
+                : ErrorCodes.FRAME_ERROR;
+        disposeCurrentMessageAndCloseConnection(errorCode, message);
+    }
+
+    @Override
+    public boolean rejectMethodFrameIfContentIncomplete()
+    {
+        if (hasCurrentMessage())
+        {
+            rejectIncompleteMessage("Method frame received before completing the previous message");
+            return true;
+        }
+        return false;
+    }
+
+    private void disposeCurrentMessageAndCloseConnection(final int errorCode, final String message)
+    {
+        disposeCurrentMessage();
+        _connection.sendConnectionClose(errorCode, message, _channelId);
+    }
+
+    private void disposeCurrentMessage()
+    {
+        final IncomingMessage currentMessage = _currentMessage;
+        _currentMessage = null;
+        if (currentMessage != null)
+        {
+            currentMessage.dispose();
+        }
+    }
+
     private void publishContentHeader(ContentHeaderBody contentHeaderBody)
     {
         if (LOGGER.isDebugEnabled())
@@ -333,13 +374,13 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
                                                 getConnection().getLastReadTime());
 
                     final MessageHandle<MessageMetaData> handle = _messageStore.addMessage(messageMetaData);
-                    int bodyCount = _currentMessage.getBodyCount();
-                    if (bodyCount > 0)
+                    final int contentChunkCount = _currentMessage.getContentChunkCount();
+                    if (contentChunkCount > 0)
                     {
-                        for (int i = 0; i < bodyCount; i++)
+                        for (int i = 0; i < contentChunkCount; i++)
                         {
-                            ContentBody contentChunk = _currentMessage.getContentChunk(i);
-                            handle.addContent(contentChunk.getPayload());
+                            final QpidByteBuffer contentChunk = _currentMessage.getContentChunk(i);
+                            handle.addContent(contentChunk);
                             contentChunk.dispose();
                         }
                     }
@@ -487,21 +528,26 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
 
     }
 
-    private void publishContentBody(ContentBody contentBody)
+    private void publishContentBody(final QpidByteBuffer contentBody)
     {
         if (LOGGER.isDebugEnabled())
         {
             LOGGER.debug(debugIdentity() + " content body received on channel " + _channelId);
         }
 
+        if (_currentMessage.getContentBodyFrameCount() >= _maxContentBodyFramesPerMessage)
+        {
+            disposeCurrentMessageAndCloseConnection(ErrorCodes.RESOURCE_ERROR,"Message exceeds maximum number of " +
+                    "content body frames (" + _maxContentBodyFramesPerMessage + ")");
+            return;
+        }
+
         try
         {
-            long currentSize = _currentMessage.addContentBodyFrame(contentBody);
-            if(currentSize > _currentMessage.getSize())
+            if (!_currentMessage.addContentBodyFrame(contentBody))
             {
-                _connection.sendConnectionClose(ErrorCodes.FRAME_ERROR,
-                                                "More message data received than content header defined",
-                                                _channelId);
+                disposeCurrentMessageAndCloseConnection(ErrorCodes.FRAME_ERROR, "More message data received than " +
+                        "content header defined");
             }
             else
             {
@@ -512,7 +558,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
         {
             // we want to make sure we don't keep a reference to the message in the
             // event of an error
-            _currentMessage = null;
+            disposeCurrentMessage();
             throw e;
         }
     }
@@ -1928,9 +1974,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
                           " immediate: " + immediate + " ]");
         }
 
-
-
-        NamedAddressSpace vHost = _connection.getAddressSpace();
+        final NamedAddressSpace vHost = _connection.getAddressSpace();
 
         if(blockingTimeoutExceeded())
         {
@@ -1940,7 +1984,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
         }
         else
         {
-            MessageDestination destination;
+            final MessageDestination destination;
 
             if (isDefaultExchange(exchangeName))
             {
@@ -1959,10 +2003,10 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
             else
             {
 
-                MessagePublishInfo info = new MessagePublishInfo(exchangeName,
-                                                                 immediate,
-                                                                 mandatory,
-                                                                 routingKey);
+                final MessagePublishInfo info = new MessagePublishInfo(exchangeName,
+                                                                       immediate,
+                                                                       mandatory,
+                                                                       routingKey);
 
                 try
                 {
@@ -2153,13 +2197,19 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
 
         if(hasCurrentMessage())
         {
-            publishContentBody(new ContentBody(data));
+            if (_currentMessage.getContentHeader() == null)
+            {
+                rejectIncompleteMessage("Attempt to send a content body before sending a content header");
+            }
+            else
+            {
+                publishContentBody(data);
+            }
         }
         else
         {
-            _connection.sendConnectionClose(ErrorCodes.COMMAND_INVALID,
-                                            "Attempt to send a content header without first sending a publish frame",
-                                            _channelId);
+            _connection.sendConnectionClose(ErrorCodes.COMMAND_INVALID, "Attempt to send a content body without " +
+                    "first sending a publish frame", _channelId);
         }
     }
 
@@ -2173,7 +2223,12 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
 
         if(hasCurrentMessage())
         {
-            if(bodySize > _connection.getMaxMessageSize())
+            if (_currentMessage.getContentHeader() != null)
+            {
+                properties.dispose();
+                rejectIncompleteMessage("Attempt to send a duplicate content header");
+            }
+            else if (bodySize > _connection.getMaxMessageSize())
             {
                 properties.dispose();
                 closeChannel(ErrorCodes.MESSAGE_TOO_LARGE,
@@ -2203,6 +2258,27 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
         }
     }
 
+    void receiveOversizedMessageHeader(final long bodySize)
+    {
+        if (hasCurrentMessage())
+        {
+            if (_currentMessage.getContentHeader() != null)
+            {
+                rejectIncompleteMessage("Attempt to send a duplicate content header");
+            }
+            else
+            {
+                closeChannel(ErrorCodes.MESSAGE_TOO_LARGE, "Content body size " + Long.toUnsignedString(bodySize) +
+                        " exceeds the supported range");
+            }
+        }
+        else
+        {
+            _connection.sendConnectionClose(ErrorCodes.COMMAND_INVALID, "Attempt to send a content header without " +
+                    "first sending a publish frame", _channelId);
+        }
+    }
+
     @Override
     public boolean ignoreAllButCloseOk()
     {
@@ -3517,24 +3593,6 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
     public void dispose()
     {
         _txCommitOkFrame.dispose();
-        final IncomingMessage currentMessage = _currentMessage;
-        if (currentMessage != null)
-        {
-            _currentMessage = null;
-            final ContentHeaderBody contentHeader = currentMessage.getContentHeader();
-            if (contentHeader != null)
-            {
-                contentHeader.dispose();
-            }
-
-            int bodyCount = currentMessage.getBodyCount();
-            if (bodyCount > 0)
-            {
-                for (int i = 0; i < bodyCount; i++)
-                {
-                    currentMessage.getContentChunk(i).dispose();
-                }
-            }
-        }
+        disposeCurrentMessage();
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java
index b2b56d407f..af0d85a243 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java
@@ -45,13 +45,14 @@ import org.apache.qpid.server.protocol.v0_8.transport.ProtocolInitiation;
  * TODO If protocol initiation decoder not needed, then don't create it. Probably not a big deal, but it adds to the
  *       per-session overhead.
  */
-public abstract class AMQDecoder<T extends MethodProcessor>
+public abstract class AMQDecoder<T extends MethodProcessor<?>>
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(AMQDecoder.class);
     public static final int FRAME_HEADER_SIZE = 7;
     public static final int FRAME_MIN_SIZE = 4096;
 
     private final T _methodProcessor;
+    private final int _maxNestedObjects;
 
     /** Holds the protocol initiation decoder. */
     private final ProtocolInitiation.Decoder _piDecoder = new ProtocolInitiation.Decoder();
@@ -62,16 +63,29 @@ public abstract class AMQDecoder<T extends MethodProcessor>
     private boolean _firstRead = true;
 
     private int _maxFrameSize = FRAME_MIN_SIZE;
+    private long _frameBytesToDiscard;
 
     /**
      * Creates a new AMQP decoder.
      * @param expectProtocolInitiation <code>true</code> if this decoder needs to handle protocol initiation.
      * @param methodProcessor method processor
      */
-    protected AMQDecoder(boolean expectProtocolInitiation, T methodProcessor)
+    protected AMQDecoder(final boolean expectProtocolInitiation, final T methodProcessor)
     {
+        this(expectProtocolInitiation, methodProcessor, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    protected AMQDecoder(final boolean expectProtocolInitiation,
+                         final T methodProcessor,
+                         final int maxNestedObjects)
+    {
+        if (maxNestedObjects < 0)
+        {
+            throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+        }
         _expectProtocolInitiation = expectProtocolInitiation;
         _methodProcessor = methodProcessor;
+        _maxNestedObjects = maxNestedObjects;
     }
 
 
@@ -97,6 +111,11 @@ public abstract class AMQDecoder<T extends MethodProcessor>
         return _methodProcessor;
     }
 
+    protected final int getMaxNestedObjects()
+    {
+        return _maxNestedObjects;
+    }
+
     protected final int decode(final QpidByteBuffer buf) throws AMQFrameDecodingException
     {
         // If this is the first read then we may be getting a protocol initiation back if we tried to negotiate
@@ -142,6 +161,11 @@ public abstract class AMQDecoder<T extends MethodProcessor>
 
     protected int decodable(final QpidByteBuffer in) throws AMQFrameDecodingException
     {
+        if (_frameBytesToDiscard != 0 && !discardFrame(in))
+        {
+            return 1;
+        }
+
         final int remainingAfterAttributes = in.remaining() - FRAME_HEADER_SIZE;
         // type, channel, body length and end byte
         if (remainingAfterAttributes < 0)
@@ -154,6 +178,8 @@ public abstract class AMQDecoder<T extends MethodProcessor>
         final long bodySize = ((long)in.getInt(in.position()+3)) & 0xffffffffL;
         if (bodySize > _maxFrameSize)
         {
+            _frameBytesToDiscard = bodySize + 1;
+            in.position(in.position() + FRAME_HEADER_SIZE);
             throw new AMQFrameDecodingException(
                     "Incoming frame size of "
                                                 + bodySize
@@ -166,6 +192,18 @@ public abstract class AMQDecoder<T extends MethodProcessor>
 
     }
 
+    private boolean discardFrame(final QpidByteBuffer in) throws AMQFatalFrameDecodingException
+    {
+        final int discarded = (int) Math.min(_frameBytesToDiscard, in.remaining());
+        in.position(in.position() + discarded);
+        _frameBytesToDiscard -= discarded;
+        if (_frameBytesToDiscard == 0 && (in.get(in.position() - 1) & 0xFF) != 0xCE)
+        {
+            throw new AMQFatalFrameDecodingException("End of frame marker not found after discarded frame");
+        }
+        return _frameBytesToDiscard == 0;
+    }
+
     protected void processInput(final QpidByteBuffer in)
             throws AMQFrameDecodingException, AMQProtocolVersionException
     {
@@ -182,16 +220,28 @@ public abstract class AMQDecoder<T extends MethodProcessor>
                                                 + " bodySize = " + bodySize);
         }
 
-        processFrame(channel, type, bodySize, in);
-
-        byte marker = in.get();
+        final int frameEnd = in.position() + (int) bodySize + 1;
+        final byte marker = in.get(frameEnd - 1);
         if ((marker & 0xFF) != 0xCE)
         {
-            throw new AMQFrameDecodingException(
+            throw new AMQFatalFrameDecodingException(
                     "End of frame marker not found. Read " + marker + " length=" + bodySize
                                                 + " type=" + type);
         }
 
+        try (final QpidByteBuffer frameBody = in.view(0, (int) bodySize))
+        {
+            processFrame(channel, type, bodySize, frameBody);
+            if (frameBody.hasRemaining())
+            {
+                throw new AMQFrameDecodingException("Frame body was not fully consumed: type=" + type +
+                        ", channel=" + channel + ", bodySize=" + bodySize + ", remaining=" + frameBody.remaining());
+            }
+        }
+        finally
+        {
+            in.position(frameEnd);
+        }
     }
 
     protected void processFrame(final int channel, final byte type, final long bodySize, final QpidByteBuffer in)
@@ -203,7 +253,7 @@ public abstract class AMQDecoder<T extends MethodProcessor>
                 processMethod(channel, in);
                 break;
             case 2:
-                ContentHeaderBody.process(in, _methodProcessor.getChannelMethodProcessor(channel), bodySize);
+                ContentHeaderBody.process(in, _methodProcessor, channel, bodySize, _maxNestedObjects);
                 break;
             case 3:
                 ContentBody.process(in, _methodProcessor.getChannelMethodProcessor(channel), bodySize);
@@ -212,7 +262,7 @@ public abstract class AMQDecoder<T extends MethodProcessor>
                 HeartbeatBody.process(channel, in, _methodProcessor, bodySize);
                 break;
             default:
-                throw new AMQFrameDecodingException("Unsupported frame type: " + type);
+                throw new AMQFatalFrameDecodingException("Unsupported frame type: " + type);
         }
     }
 
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFatalFrameDecodingException.java
similarity index 55%
copy from broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
copy to broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFatalFrameDecodingException.java
index c8a7db57ae..945f7da5e2 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFatalFrameDecodingException.java
@@ -20,29 +20,16 @@
  */
 package org.apache.qpid.server.protocol.v0_8;
 
-import org.apache.qpid.server.AMQException;
-import org.apache.qpid.server.protocol.ErrorCodes;
+import java.io.Serial;
 
-/**
- * AMQFrameDecodingException indicates that an AMQP frame cannot be decoded because it does not have the correct
- * format as defined by the protocol.
- */
-public class AMQFrameDecodingException extends AMQException
+/** Indicates framing corruption for which AMQP prohibits sending any further data on the connection */
+final class AMQFatalFrameDecodingException extends AMQFrameDecodingException
 {
-    public AMQFrameDecodingException(String message, Throwable cause)
-    {
-        super(ErrorCodes.FRAME_ERROR, message, cause);
-    }
-
-    public AMQFrameDecodingException(String message)
-    {
-        super(ErrorCodes.FRAME_ERROR, message, null);
-    }
+    @Serial
+    private static final long serialVersionUID = 1L;
 
-
-    public AMQFrameDecodingException(int errorCode, String message, final Throwable cause)
+    AMQFatalFrameDecodingException(final String message)
     {
-        super(errorCode, message, cause);
+        super(message);
     }
-
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
index c8a7db57ae..246c68badb 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
@@ -23,26 +23,73 @@ package org.apache.qpid.server.protocol.v0_8;
 import org.apache.qpid.server.AMQException;
 import org.apache.qpid.server.protocol.ErrorCodes;
 
+import java.io.Serial;
+
 /**
  * AMQFrameDecodingException indicates that an AMQP frame cannot be decoded because it does not have the correct
  * format as defined by the protocol.
  */
 public class AMQFrameDecodingException extends AMQException
 {
-    public AMQFrameDecodingException(String message, Throwable cause)
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private final int _classId;
+    private final int _methodId;
+
+    public AMQFrameDecodingException(final String message, final Throwable cause)
     {
-        super(ErrorCodes.FRAME_ERROR, message, cause);
+        this(ErrorCodes.FRAME_ERROR, message, 0, 0, cause);
     }
 
-    public AMQFrameDecodingException(String message)
+    public AMQFrameDecodingException(final String message)
     {
-        super(ErrorCodes.FRAME_ERROR, message, null);
+        this(ErrorCodes.FRAME_ERROR, message, 0, 0, null);
     }
 
 
-    public AMQFrameDecodingException(int errorCode, String message, final Throwable cause)
+    public AMQFrameDecodingException(final int errorCode, final String message, final Throwable cause)
+    {
+        this(errorCode, message, 0, 0, cause);
+    }
+
+    AMQFrameDecodingException(final int errorCode,
+                              final String message,
+                              final int classId,
+                              final int methodId,
+                              final Throwable cause)
     {
         super(errorCode, message, cause);
+        _classId = classId;
+        _methodId = methodId;
+    }
+
+    public static AMQFrameDecodingException forDecodingFailure(final String context,
+                                                               final RuntimeException cause)
+    {
+        return forDecodingFailure(context, 0, 0, cause);
     }
 
+    static AMQFrameDecodingException forDecodingFailure(final String context,
+                                                        final int classId,
+                                                        final int methodId,
+                                                        final RuntimeException cause)
+    {
+        final int errorCode = cause instanceof AMQValueNestingException
+                ? ErrorCodes.RESOURCE_ERROR
+                : ErrorCodes.FRAME_ERROR;
+        final String detail = cause.getMessage();
+        final String message = detail == null || detail.isEmpty() ? context : context + ": " + detail;
+        return new AMQFrameDecodingException(errorCode, message, classId, methodId, cause);
+    }
+
+    int getClassId()
+    {
+        return _classId;
+    }
+
+    int getMethodId()
+    {
+        return _methodId;
+    }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java
index 1943dfaa0d..cb7cb3d47b 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java
@@ -56,6 +56,17 @@ public interface AMQPConnection_0_8<C extends AMQPConnection_0_8<C>> extends AMQ
     @ManagedContextDefault(name= FORCE_MESSAGE_VALIDATION)
     boolean DEFAULT_FORCE_MESSAGE_VALIDATION = false;
 
+    String CODEC_MAX_NESTED_OBJECTS = "amqp0x.codec.maxNestedObjects";
+    @ManagedContextDefault(name = CODEC_MAX_NESTED_OBJECTS,
+            description = "Maximum nesting depth of AMQP 0-x field tables and arrays")
+    int DEFAULT_CODEC_MAX_NESTED_OBJECTS = 50;
+
+    String CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE = "connection.maxContentBodyFramesPerMessage";
+    @ManagedContextDefault(name = CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE,
+            description = "Maximum number of content body frames accepted for one message. The default preserves " +
+                    "the default maximum message size for peers using the AMQP minimum frame size.")
+    int DEFAULT_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE = 256 * 1024;
+
     @DerivedAttribute(description = "The actual negotiated value of heartbeat delay.")
     int getHeartbeatDelay();
 
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
index f706ccf705..6a4ca494fc 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
@@ -24,10 +24,10 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import java.io.IOException;
 import java.lang.reflect.Proxy;
-import java.nio.BufferUnderflowException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
@@ -40,7 +40,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
 
-import org.apache.qpid.server.security.limit.ConnectionLimitException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -79,6 +78,7 @@ import org.apache.qpid.server.security.AccessDeniedException;
 import org.apache.qpid.server.security.SubjectCreator;
 import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
 import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
 import org.apache.qpid.server.session.AMQPSession;
 import org.apache.qpid.server.transport.AbstractAMQPConnection;
 import org.apache.qpid.server.transport.AggregateTicker;
@@ -90,6 +90,7 @@ import org.apache.qpid.server.txn.LocalTransaction;
 import org.apache.qpid.server.txn.ServerTransaction;
 import org.apache.qpid.server.util.Action;
 import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
 import org.apache.qpid.server.virtualhost.VirtualHostUnavailableException;
 
 public class AMQPConnection_0_8Impl
@@ -129,6 +130,7 @@ public class AMQPConnection_0_8Impl
 
     private final ServerDecoder _decoder;
 
+    private List<String> _advertisedSaslMechanisms = Collections.emptyList();
     private volatile SaslNegotiator _saslNegotiator;
 
     private volatile int _maxNoOfChannels;
@@ -168,6 +170,7 @@ public class AMQPConnection_0_8Impl
     private volatile int _currentMethodId;
     private final int _binaryDataLimit;
     private volatile boolean _transportBlockedForWriting;
+    private volatile boolean _inputClosed;
     private volatile SubjectAuthenticationResult _successfulAuthenticationResult;
 
     private final Set<AMQPSession<?,?>> _sessionsWithWork =
@@ -255,16 +258,88 @@ public class AMQPConnection_0_8Impl
     @Override
     protected void onReceive(final QpidByteBuffer msg)
     {
-        try
+        if (_inputClosed)
         {
-            _decoder.decodeBuffer(msg);
-            receivedCompleteAllChannels();
+            msg.position(msg.limit());
+            return;
         }
-        catch (AMQFrameDecodingException | IOException | AMQPInvalidClassException
-                | IllegalArgumentException | IllegalStateException | BufferUnderflowException e)
+
+        while (true)
         {
-            LOGGER.warn("Unexpected exception", e);
-            throw new ConnectionScopedRuntimeException(e);
+            final int position = msg.position();
+            try
+            {
+                _decoder.decodeBuffer(msg);
+                receivedCompleteAllChannels();
+                if (_inputClosed)
+                {
+                    msg.position(msg.limit());
+                }
+                return;
+            }
+            catch (final AMQValueNestingException e)
+            {
+                LOGGER.debug("Field-value nesting limit exceeded", e);
+                handleProtocolError(ErrorCodes.RESOURCE_ERROR, e.getMessage());
+            }
+            catch (final AMQFatalFrameDecodingException e)
+            {
+                LOGGER.warn("Fatal frame decoding error", e);
+                msg.position(msg.limit());
+                closeNetworkConnection();
+                return;
+            }
+            catch (final AMQFrameDecodingException e)
+            {
+                LOGGER.debug("Frame decoding error", e);
+                handleProtocolError(e.getErrorCode(), e.getMessage(), e.getClassId(), e.getMethodId());
+            }
+            catch (final ConnectionScopedRuntimeException | ServerScopedRuntimeException e)
+            {
+                throw e;
+            }
+            catch (final IOException | RuntimeException e)
+            {
+                LOGGER.warn("Unexpected exception", e);
+                _inputClosed = true;
+                msg.position(msg.limit());
+                throw new ConnectionScopedRuntimeException(e);
+            }
+
+            if (_inputClosed || msg.position() <= position)
+            {
+                msg.position(msg.limit());
+                if (!_inputClosed)
+                {
+                    closeNetworkConnection();
+                }
+                return;
+            }
+
+            if (!isClosing() || !msg.hasRemaining())
+            {
+                return;
+            }
+        }
+    }
+
+    private void handleProtocolError(final int errorCode, final String message)
+    {
+        handleProtocolError(errorCode, message, 0, 0);
+    }
+
+    private void handleProtocolError(final int errorCode,
+                                     final String message,
+                                     final int classId,
+                                     final int methodId)
+    {
+        if (_state == ConnectionState.OPEN)
+        {
+            sendConnectionClose(errorCode, message, 0, classId, methodId);
+        }
+        else
+        {
+            closeNetworkConnection();
         }
     }
 
@@ -303,27 +378,21 @@ public class AMQPConnection_0_8Impl
         _channelsForCurrentMessage.add(amqChannel);
     }
 
-    private synchronized void protocolInitiationReceived(ProtocolInitiation pi)
+    private synchronized void protocolInitiationReceived(final ProtocolInitiation pi)
     {
         // this ensures the codec never checks for a PI message again
         _decoder.setExpectProtocolInitiation(false);
         try
         {
-            ProtocolVersion pv = pi.checkVersion(); // Fails if not correct
+            final ProtocolVersion pv = pi.checkVersion(); // Fails if not correct
             setProtocolVersion(pv);
 
-            StringBuilder mechanismBuilder = new StringBuilder();
-            for(String mechanismName : getPort().getAuthenticationProvider().getAvailableMechanisms(getTransport().isSecure()))
-            {
-                if(mechanismBuilder.length() != 0)
-                {
-                    mechanismBuilder.append(' ');
-                }
-                mechanismBuilder.append(mechanismName);
-            }
-            String mechanisms = mechanismBuilder.toString();
+            final List<String> advertisedSaslMechanisms = List.copyOf(getPort().getAuthenticationProvider()
+                    .getAvailableMechanisms(getTransport().isSecure()));
+            _advertisedSaslMechanisms = advertisedSaslMechanisms;
+            final String mechanisms = String.join(" ", advertisedSaslMechanisms);
 
-            String locales = "en_US";
+            final String locales = "en_US";
 
             Map<String,Object> props = Map.of();
             for(ConnectionPropertyEnricher enricher : getPort().getConnectionPropertyEnrichers())
@@ -517,13 +586,23 @@ public class AMQPConnection_0_8Impl
     }
 
     @Override
-    public void sendConnectionClose(int errorCode,
-                                    String message, int channelId)
+    public void sendConnectionClose(final int errorCode, final String message, final int channelId)
     {
-        sendConnectionClose(channelId, new AMQFrame(0, new ConnectionCloseBody(getProtocolVersion(), errorCode, AMQShortString.validValueOf(message), _currentClassId, _currentMethodId)));
+        sendConnectionClose(errorCode, message, channelId, _currentClassId, _currentMethodId);
     }
 
-    private void sendConnectionClose(int channelId, AMQFrame frame)
+    protected void sendConnectionClose(final int errorCode,
+                                       final String message,
+                                       final int channelId,
+                                       final int classId,
+                                       final int methodId)
+    {
+        final ConnectionCloseBody connectionCloseBody = new ConnectionCloseBody(getProtocolVersion(), errorCode,
+                AMQShortString.validValueOf(message), classId, methodId);
+        sendConnectionClose(channelId, new AMQFrame(0, connectionCloseBody));
+    }
+
+    private void sendConnectionClose(final int channelId, final AMQFrame frame)
     {
         if (_orderlyClose.compareAndSet(false, true))
         {
@@ -553,6 +632,7 @@ public class AMQPConnection_0_8Impl
 
     public void closeNetworkConnection()
     {
+        _inputClosed = true;
         getNetwork().close();
     }
 
@@ -653,6 +733,7 @@ public class AMQPConnection_0_8Impl
     @Override
     public void closed()
     {
+        _inputClosed = true;
         try
         {
             try
@@ -1092,14 +1173,24 @@ public class AMQPConnection_0_8Impl
 
         LOGGER.debug("SASL Mechanism selected: {} Locale : {}", mechanism, locale);
 
+        final List<String> advertisedSaslMechanisms = _advertisedSaslMechanisms;
+        _advertisedSaslMechanisms = Collections.emptyList();
+
         if (mechanism == null || mechanism.length() == 0)
         {
-            sendConnectionClose(ErrorCodes.CONNECTION_FORCED, "No Sasl mechanism was specified", 0);
+            closeNetworkConnection();
             return;
         }
 
-        SubjectCreator subjectCreator = getSubjectCreator();
-        _saslNegotiator = subjectCreator.createSaslNegotiator(String.valueOf(mechanism), this);
+        final String mechanismName = mechanism.toString();
+        if (!advertisedSaslMechanisms.contains(mechanismName))
+        {
+            closeNetworkConnection();
+            return;
+        }
+
+        final SubjectCreator subjectCreator = getSubjectCreator();
+        _saslNegotiator = subjectCreator.createSaslNegotiator(mechanismName, this);
         if (_saslNegotiator == null)
         {
             sendConnectionClose(ErrorCodes.CONNECTION_FORCED, "No SaslServer could be created for mechanism: " + mechanism, 0);
@@ -1308,12 +1399,37 @@ public class AMQPConnection_0_8Impl
                         {
                             return channelAwaitingClosure(channelId);
                         }
+                        else if (method.getName().equals("rejectMethodFrameIfContentIncomplete"))
+                        {
+                            return false;
+                        }
                         return null;
                     });
         }
         return channelMethodProcessor;
     }
 
+    @Override
+    public void receiveOversizedMessageHeader(final int channelId, final long bodySize)
+            throws AMQFrameDecodingException
+    {
+        if (_state != ConnectionState.OPEN)
+        {
+            throw new AMQFrameDecodingException("Content body size " + Long.toUnsignedString(bodySize) +
+                    " exceeds the supported range", null);
+        }
+
+        final AMQChannel channel = getChannel(channelId);
+        if (channel != null)
+        {
+            channel.receiveOversizedMessageHeader(bodySize);
+        }
+        else if (!channelAwaitingClosure(channelId))
+        {
+            sendConnectionClose(ErrorCodes.CHANNEL_ERROR, "Unknown channel id: " + channelId, channelId);
+        }
+    }
+
     @Override
     public void receiveHeartbeat()
     {
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java
index 442a2c5c1d..ccf2125bae 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java
@@ -302,9 +302,36 @@ public enum AMQType
          * @return An instance of the type.
          */
         @Override
-        public Object readValueFromBuffer(QpidByteBuffer buffer)
+        public Object readValueFromBuffer(final QpidByteBuffer buffer)
         {
-            return EncodingUtils.readFieldTable(buffer);
+            return readValueFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+        }
+
+        @Override
+        public Object readValueFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+        {
+            final FieldTable fieldTable = EncodingUtils.readFieldTableValue(buffer, maxNestedObjects);
+            if (fieldTable == null)
+            {
+                return null;
+            }
+
+            if (!fieldTable.isDirect())
+            {
+                return fieldTable;
+            }
+
+            try
+            {
+                try (final QpidByteBuffer encodedForm = QpidByteBuffer.wrap(fieldTable.getDataAsBytes()))
+                {
+                    return FieldTableFactory.createFieldTable(encodedForm, encodedForm.remaining(), maxNestedObjects);
+                }
+            }
+            finally
+            {
+                fieldTable.dispose();
+            }
         }
 
         @Override
@@ -367,11 +394,16 @@ public enum AMQType
                  * @return An instance of the type.
                  */
                 @Override
-                public Object readValueFromBuffer(QpidByteBuffer buffer)
+                public Object readValueFromBuffer(final QpidByteBuffer buffer)
                 {
-                    // Read size of field table then all name/value pairs.
-                    return FieldArray.readFromBuffer(buffer);
+                    return readValueFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+                }
 
+                @Override
+                public Object readValueFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+                {
+                    // read size of field array then all values
+                    return FieldArray.readFromBuffer(buffer, maxNestedObjects);
                 }
 
                 @Override
@@ -1105,5 +1137,10 @@ public enum AMQType
      */
     abstract Object readValueFromBuffer(QpidByteBuffer buffer);
 
+    Object readValueFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+    {
+        return readValueFromBuffer(buffer);
+    }
+
     abstract void skip(QpidByteBuffer buffer);
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java
index 6836c3eb89..eb902c41a3 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java
@@ -64,10 +64,12 @@ public abstract class AMQTypedValue
             _value = type.toNativeValue(value);
         }
 
-        private GenericTypedValue(AMQType type, QpidByteBuffer buffer)
+        private GenericTypedValue(final AMQType type,
+                                  final QpidByteBuffer buffer,
+                                  final int maxNestedObjects)
         {
             _type = type;
-            _value = type.readValueFromBuffer(buffer);
+            _value = type.readValueFromBuffer(buffer, maxNestedObjects);
         }
 
 
@@ -211,21 +213,26 @@ public abstract class AMQTypedValue
     }
 
 
-    public static AMQTypedValue readFromBuffer(QpidByteBuffer buffer)
+    public static AMQTypedValue readFromBuffer(final QpidByteBuffer buffer)
     {
-        AMQType type = AMQTypeMap.getType(buffer.get());
+        return readFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
 
-        switch(type)
+    static AMQTypedValue readFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+    {
+        final AMQType type = AMQTypeMap.getType(buffer.get());
+
+        switch (type)
         {
             case LONG:
                 return new LongTypedValue(buffer);
 
             case INT:
-                int value = buffer.getInt();
+                final int value = buffer.getInt();
                 return createAMQTypedValue(value);
 
             default:
-                return new GenericTypedValue(type, buffer);
+                return new GenericTypedValue(type, buffer, maxNestedObjects);
         }
 
     }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQValueNestingException.java
similarity index 55%
copy from broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
copy to broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQValueNestingException.java
index c8a7db57ae..f33c237068 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQValueNestingException.java
@@ -20,29 +20,15 @@
  */
 package org.apache.qpid.server.protocol.v0_8;
 
-import org.apache.qpid.server.AMQException;
-import org.apache.qpid.server.protocol.ErrorCodes;
+import java.io.Serial;
 
-/**
- * AMQFrameDecodingException indicates that an AMQP frame cannot be decoded because it does not have the correct
- * format as defined by the protocol.
- */
-public class AMQFrameDecodingException extends AMQException
+final class AMQValueNestingException extends IllegalArgumentException
 {
-    public AMQFrameDecodingException(String message, Throwable cause)
-    {
-        super(ErrorCodes.FRAME_ERROR, message, cause);
-    }
-
-    public AMQFrameDecodingException(String message)
-    {
-        super(ErrorCodes.FRAME_ERROR, message, null);
-    }
+    @Serial
+    private static final long serialVersionUID = 1L;
 
-
-    public AMQFrameDecodingException(int errorCode, String message, final Throwable cause)
+    AMQValueNestingException(final String message)
     {
-        super(errorCode, message, cause);
+        super(message);
     }
-
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java
index 5dccce7dbb..90a9c6e37e 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java
@@ -38,10 +38,17 @@ public class BrokerDecoder extends ServerDecoder
      */
     public BrokerDecoder(final AMQPConnection_0_8Impl connection)
     {
-        super(connection);
+        super(connection, getMaxNestedObjects(connection));
         _connection = connection;
     }
 
+    private static int getMaxNestedObjects(final AMQPConnection_0_8Impl connection)
+    {
+        final Integer configured = connection.getContextValue(Integer.class,
+                AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS);
+        return configured == null ? AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS : configured;
+    }
+
     @Override
     protected void processFrame(final int channelId, final byte type, final long bodySize, final QpidByteBuffer in)
             throws AMQFrameDecodingException
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java
index 0b9ff41923..0fd92a006b 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java
@@ -20,6 +20,8 @@
  */
 package org.apache.qpid.server.protocol.v0_8;
 
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
 import java.nio.charset.StandardCharsets;
 
 import org.slf4j.Logger;
@@ -35,6 +37,16 @@ public class EncodingUtils
     {
     }
 
+    static int checkLength(final long length, final QpidByteBuffer buffer)
+    {
+        if (length > buffer.remaining())
+        {
+            throw new IllegalArgumentException("Declared field length " + length +
+                    " is invalid; buffer has " + buffer.remaining() + " byte(s) remaining");
+        }
+        return (int) length;
+    }
+
     public static int encodedShortStringLength(String s)
     {
         if (s == null)
@@ -206,14 +218,38 @@ public class EncodingUtils
 
     public static FieldTable readFieldTable(QpidByteBuffer input)
     {
-        long length = input.getUnsignedInt();
+        return readFieldTable(input, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static FieldTable readFieldTable(final QpidByteBuffer input, final int maxNestedObjects)
+    {
+        FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+        final FieldTable fieldTable = readFieldTableValue(input, maxNestedObjects);
+        if (fieldTable != null)
+        {
+            try
+            {
+                fieldTable.validate(maxNestedObjects);
+            }
+            catch (RuntimeException e)
+            {
+                fieldTable.dispose();
+                throw e;
+            }
+        }
+        return fieldTable;
+    }
+
+    static FieldTable readFieldTableValue(final QpidByteBuffer input, final int maxNestedObjects)
+    {
+        final long length = input.getUnsignedInt();
         if (length == 0)
         {
             return null;
         }
         else
         {
-            return FieldTableFactory.createFieldTable(input, (int) length);
+            return FieldTableFactory.createFieldTable(input, checkLength(length, input), maxNestedObjects);
         }
     }
 
@@ -222,24 +258,31 @@ public class EncodingUtils
         long length = buffer.getUnsignedInt();
         if (length > 0)
         {
-            buffer.position(buffer.position() + (int)length);
+            buffer.position(buffer.position() + checkLength(length, buffer));
         }
     }
 
 
-    public static String readLongString(QpidByteBuffer buffer)
+    public static String readLongString(final QpidByteBuffer buffer)
     {
-        long length = ((long)(buffer.getInt())) & 0xFFFFFFFFL;
+        final long length = ((long)(buffer.getInt())) & 0xFFFFFFFFL;
         if (length == 0)
         {
             return "";
         }
         else
         {
-            byte[] stringBytes = new byte[(int) length];
-            buffer.get(stringBytes, 0, (int) length);
+            final byte[] stringBytes = new byte[checkLength(length, buffer)];
+            buffer.get(stringBytes, 0, stringBytes.length);
 
-            return new String(stringBytes, StandardCharsets.UTF_8);
+            try
+            {
+                return StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(stringBytes)).toString();
+            }
+            catch (CharacterCodingException e)
+            {
+                throw new IllegalArgumentException("Cannot decode long string as UTF-8", e);
+            }
         }
     }
 
@@ -249,7 +292,7 @@ public class EncodingUtils
         long length = buffer.getUnsignedInt();
         if (length > 0)
         {
-            buffer.position(buffer.position() + (int)length);
+            buffer.position(buffer.position() + checkLength(length, buffer));
         }
     }
 
@@ -262,7 +305,7 @@ public class EncodingUtils
         }
         else
         {
-            byte[] result = new byte[(int) length];
+            final byte[] result = new byte[checkLength(length, buffer)];
             buffer.get(result);
 
             return result;
@@ -322,7 +365,7 @@ public class EncodingUtils
         }
         else
         {
-            byte[] dataBytes = new byte[(int)length];
+            final byte[] dataBytes = new byte[checkLength(length, buffer)];
             buffer.get(dataBytes);
 
             return dataBytes;
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java
index 8b55a5cd04..419ef2197a 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java
@@ -115,14 +115,19 @@ public class FieldArray<T> extends AbstractCollection<T>
 
     public static FieldArray<?> readFromBuffer(final QpidByteBuffer buffer)
     {
-        ArrayList<Object> result = new ArrayList<>();
-        int size = buffer.getInt();
+        return readFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    static FieldArray<?> readFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+    {
+        final ArrayList<Object> result = new ArrayList<>();
+        final int size = EncodingUtils.checkLength(buffer.getUnsignedInt(), buffer);
         try (QpidByteBuffer slicedBuffer = buffer.view(0, size))
         {
             buffer.position(buffer.position() + size);
             while (slicedBuffer.hasRemaining())
             {
-                result.add(AMQTypedValue.readFromBuffer(slicedBuffer).getValue());
+                result.add(AMQTypedValue.readFromBuffer(slicedBuffer, maxNestedObjects).getValue());
             }
         }
         return new FieldArray<>(result);
@@ -130,7 +135,7 @@ public class FieldArray<T> extends AbstractCollection<T>
 
     public static void skipFieldArray(final QpidByteBuffer buffer)
     {
-        int size = buffer.getInt();
+        final int size = EncodingUtils.checkLength(buffer.getUnsignedInt(), buffer);
         if (size > 0)
         {
             buffer.position(buffer.position() + size);
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java
index b5ad4db480..35876132ea 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java
@@ -47,20 +47,29 @@ public class FieldTable
     static boolean _strictAMQP = Boolean.valueOf(System.getProperty(STRICT_AMQP_NAME, "false"));
 
     private final FieldTableSupport _fieldTableSupport;
+    private final int _maxNestedObjects;
 
-    FieldTable(QpidByteBuffer input, int len)
+    FieldTable(final QpidByteBuffer input, final int len)
     {
+        this(input, len, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    FieldTable(final QpidByteBuffer input, final int len, final int maxNestedObjects)
+    {
+        FieldValueNestingValidator.validateMaximum(maxNestedObjects);
         final QpidByteBuffer encodedForm = input.view(0, len);
         input.position(input.position() + len);
         _fieldTableSupport = new ByteBufferFieldTableSupport(encodedForm);
+        _maxNestedObjects = maxNestedObjects;
     }
 
-    FieldTable(QpidByteBuffer buffer)
+    FieldTable(final QpidByteBuffer buffer)
     {
+        _maxNestedObjects = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS;
         _fieldTableSupport = new ByteBufferFieldTableSupport(buffer.duplicate());
     }
 
-    FieldTable(Map<String, Object> properties)
+    FieldTable(final Map<String, Object> properties)
     {
         final Map<String, AMQTypedValue> m;
         if (properties != null && !properties.isEmpty())
@@ -79,11 +88,19 @@ public class FieldTable
         }
 
         _fieldTableSupport = new MapFieldTableSupport(m);
+        _maxNestedObjects = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS;
+    }
+
+    FieldTable(final FieldTableSupport fieldTableSupport)
+    {
+        this(fieldTableSupport, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
     }
 
-    FieldTable(FieldTableSupport fieldTableSupport)
+    FieldTable(final FieldTableSupport fieldTableSupport, final int maxNestedObjects)
     {
-        _fieldTableSupport = new MapFieldTableSupport(fieldTableSupport.getAsMap());
+        FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+        _fieldTableSupport = new MapFieldTableSupport(fieldTableSupport.getAsMap(maxNestedObjects));
+        _maxNestedObjects = maxNestedObjects;
     }
 
     private static AMQTypedValue getAMQTypeValue(final Object object) throws AMQPInvalidClassException
@@ -179,10 +196,10 @@ public class FieldTable
     @Override
     public String toString()
     {
-        return _fieldTableSupport.toString();
+        return getProperties().toString();
     }
 
-    private static void checkPropertyName(String propertyName)
+    static void checkPropertyName(final String propertyName)
     {
         if (propertyName == null)
         {
@@ -266,14 +283,43 @@ public class FieldTable
     }
 
     public static Map<String, Object> convertToMap(final FieldTable fieldTable)
+    {
+        final int maxNestedObjects = fieldTable == null
+                ? AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS
+                : fieldTable._maxNestedObjects;
+        return convertToMap(fieldTable, maxNestedObjects);
+    }
+
+    public static Map<String, Object> convertToMap(final FieldTable fieldTable, final int maxNestedObjects)
+    {
+        if (maxNestedObjects < 0)
+        {
+            throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+        }
+        if (fieldTable != null)
+        {
+            fieldTable.validateNesting(maxNestedObjects);
+        }
+        return convertToMap(fieldTable, 1, maxNestedObjects);
+    }
+
+    private static Map<String, Object> convertToMap(final FieldTable fieldTable,
+                                                    final int depth,
+                                                    final int maxNestedObjects)
     {
         if (fieldTable != null)
         {
+            if (depth > maxNestedObjects)
+            {
+                throw new AMQValueNestingException("Maximum field-value nesting depth (" +
+                        maxNestedObjects + ") exceeded");
+            }
+
             final Map<String, Object> map = new LinkedHashMap<>();
-            Map<String, AMQTypedValue> properties = fieldTable.getProperties();
+            final Map<String, AMQTypedValue> properties = fieldTable.getProperties();
             if (properties != null)
             {
-                for (Map.Entry<String, AMQTypedValue> e : properties.entrySet())
+                for (final Map.Entry<String, AMQTypedValue> e : properties.entrySet())
                 {
                     Object val = e.getValue().getValue();
                     if (val instanceof AMQShortString)
@@ -282,7 +328,12 @@ public class FieldTable
                     }
                     else if (val instanceof FieldTable)
                     {
-                        val = FieldTable.convertToMap((FieldTable) val);
+                        if (depth >= maxNestedObjects)
+                        {
+                            throw new AMQValueNestingException("Maximum field-value nesting depth (" +
+                                    maxNestedObjects + ") exceeded");
+                        }
+                        val = convertToMap((FieldTable) val, depth + 1, maxNestedObjects);
                     }
                     map.put(e.getKey(), val);
                 }
@@ -307,9 +358,9 @@ public class FieldTable
         return getEncodedSize() > 0;
     }
 
-    public boolean containsKey(String key)
+    public boolean containsKey(final String key)
     {
-        return _fieldTableSupport.containsKey(key);
+        return _fieldTableSupport.containsKey(key, _maxNestedObjects);
     }
 
     public Set<String> keys()
@@ -317,10 +368,10 @@ public class FieldTable
         return new LinkedHashSet<>(getProperties().keySet());
     }
 
-    public Object get(String key)
+    public Object get(final String key)
     {
         checkPropertyName(key);
-        return _fieldTableSupport.get(key);
+        return _fieldTableSupport.get(key, _maxNestedObjects);
     }
 
     @Override
@@ -348,7 +399,7 @@ public class FieldTable
 
     private Map<String, AMQTypedValue> getProperties()
     {
-        return _fieldTableSupport.getAsMap();
+        return _fieldTableSupport.getAsMap(_maxNestedObjects);
     }
 
     public static FieldTable convertToFieldTable(Map<String, Object> map)
@@ -370,31 +421,88 @@ public class FieldTable
             return null;
         }
 
-        return new FieldTable(fieldTable._fieldTableSupport);
+        fieldTable.validate();
+        if (fieldTable.isDirect())
+        {
+            try (final QpidByteBuffer encodedForm = QpidByteBuffer.wrap(fieldTable.getDataAsBytes()))
+            {
+                final FieldTable detachedTable = new FieldTable(encodedForm, encodedForm.remaining(),
+                        fieldTable._maxNestedObjects);
+                try
+                {
+                    detachedTable._fieldTableSupport.markValidated(fieldTable._maxNestedObjects);
+                    return new FieldTable(detachedTable._fieldTableSupport, fieldTable._maxNestedObjects);
+                }
+                finally
+                {
+                    detachedTable.dispose();
+                }
+            }
+        }
+
+        return new FieldTable(fieldTable._fieldTableSupport, fieldTable._maxNestedObjects);
+    }
+
+    boolean isDirect()
+    {
+        return _fieldTableSupport.isDirect();
+    }
+
+    private static void markValueValidated(final Object value, final int maxNestedObjects)
+    {
+        if (value instanceof FieldTable)
+        {
+            ((FieldTable) value)._fieldTableSupport.markValidated(maxNestedObjects);
+        }
+        else if (value instanceof Collection)
+        {
+            for (final Object element : (Collection<?>) value)
+            {
+                markValueValidated(element, maxNestedObjects);
+            }
+        }
     }
 
     public void validate()
     {
-        _fieldTableSupport.validate();
+        validate(_maxNestedObjects);
+    }
+
+    public void validate(final int maxNestedObjects)
+    {
+        FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+        _fieldTableSupport.validate(maxNestedObjects);
+    }
+
+    void validateNesting(final int maxNestedObjects)
+    {
+        FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+        _fieldTableSupport.validateNesting(maxNestedObjects);
     }
 
     interface FieldTableSupport
     {
-        Object get(String key);
+        Object get(final String key, final int maxNestedObjects);
 
-        boolean containsKey(String key);
+        boolean containsKey(final String key, final int maxNestedObjects);
 
         long getEncodedSize();
 
-        void writeToBuffer(QpidByteBuffer buffer);
+        void writeToBuffer(final QpidByteBuffer buffer);
 
         byte[] getAsBytes();
 
-        Map<String, AMQTypedValue> getAsMap();
+        Map<String, AMQTypedValue> getAsMap(final int maxNestedObjects);
 
         void dispose();
 
-        void validate();
+        void validate(final int maxNestedObjects);
+
+        boolean isDirect();
+
+        void markValidated(final int maxNestedObjects);
+
+        void validateNesting(final int maxNestedObjects);
     }
 
     static class ByteBufferFieldTableSupport implements FieldTableSupport
@@ -403,6 +511,7 @@ public class FieldTable
 
         private final QpidByteBuffer _encodedForm;
         private volatile SoftReference<Map<String, AMQTypedValue>> _cache;
+        private volatile int _validatedMaxNestedObjects = -1;
 
         ByteBufferFieldTableSupport(final QpidByteBuffer encodedForm)
         {
@@ -417,9 +526,10 @@ public class FieldTable
         }
 
         @Override
-        public synchronized Object get(final String key)
+        public synchronized Object get(final String key, final int maxNestedObjects)
         {
-            final AMQTypedValue value = getValue(key);
+            validateEncodedForm(maxNestedObjects);
+            final AMQTypedValue value = getValue(key, maxNestedObjects);
             if (value != null && value != NOT_PRESENT)
             {
                 return value.getValue();
@@ -431,9 +541,10 @@ public class FieldTable
         }
 
         @Override
-        public boolean containsKey(final String key)
+        public boolean containsKey(final String key, final int maxNestedObjects)
         {
-            final AMQTypedValue value = getValue(key);
+            validateEncodedForm(maxNestedObjects);
+            final AMQTypedValue value = getValue(key, maxNestedObjects);
             return value != null && value != NOT_PRESENT;
         }
 
@@ -454,9 +565,9 @@ public class FieldTable
         }
 
         @Override
-        public Map<String, AMQTypedValue> getAsMap()
+        public Map<String, AMQTypedValue> getAsMap(final int maxNestedObjects)
         {
-            return decode();
+            return decode(maxNestedObjects);
         }
 
         @Override
@@ -470,9 +581,36 @@ public class FieldTable
         }
 
         @Override
-        public void validate()
+        public void validate(final int maxNestedObjects)
+        {
+            validateEncodedForm(maxNestedObjects);
+        }
+
+        @Override
+        public boolean isDirect()
+        {
+            return _encodedForm.isDirect();
+        }
+
+        @Override
+        public synchronized void markValidated(final int maxNestedObjects)
+        {
+            FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+            _validatedMaxNestedObjects = _validatedMaxNestedObjects < 0
+                    ? maxNestedObjects
+                    : Math.min(_validatedMaxNestedObjects, maxNestedObjects);
+        }
+
+        @Override
+        public synchronized void validateNesting(final int maxNestedObjects)
         {
-            decode();
+            if (_validatedMaxNestedObjects < 0 || maxNestedObjects < _validatedMaxNestedObjects)
+            {
+                if (FieldValueNestingValidator.mayExceedMaximumDepth(_encodedForm.remaining(), maxNestedObjects))
+                {
+                    validateEncodedForm(maxNestedObjects);
+                }
+            }
         }
 
         @Override
@@ -498,14 +636,7 @@ public class FieldTable
             return _encodedForm.hashCode();
         }
 
-        @Override
-        public String toString()
-        {
-            return getAsMap().toString();
-        }
-
-
-        private synchronized AMQTypedValue getValue(final String key)
+        private synchronized AMQTypedValue getValue(final String key, final int maxNestedObjects)
         {
             AMQTypedValue value = null;
             Map<String, AMQTypedValue> properties = _cache.get();
@@ -520,7 +651,7 @@ public class FieldTable
             }
             if (value == null)
             {
-                value = findValueForKey(key);
+                value = findValueForKey(key, maxNestedObjects);
                 if (value == null)
                 {
                     value = NOT_PRESENT;
@@ -533,9 +664,9 @@ public class FieldTable
             return value;
         }
 
-        private AMQTypedValue findValueForKey(String key)
+        private AMQTypedValue findValueForKey(final String key, final int maxNestedObjects)
         {
-            byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+            final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
             _encodedForm.mark();
             try
             {
@@ -544,11 +675,13 @@ public class FieldTable
                     final byte[] bytes = AMQShortString.readAMQShortStringAsBytes(_encodedForm);
                     if (Arrays.equals(keyBytes, bytes))
                     {
-                        return AMQTypedValue.readFromBuffer(_encodedForm);
+                        final AMQTypedValue value = AMQTypedValue.readFromBuffer(_encodedForm, maxNestedObjects);
+                        markValueValidated(value.getValue(), _validatedMaxNestedObjects);
+                        return value;
                     }
                     else
                     {
-                        AMQType type = AMQTypeMap.getType(_encodedForm.get());
+                        final AMQType type = AMQTypeMap.getType(_encodedForm.get());
                         type.skip(_encodedForm);
                     }
                 }
@@ -560,8 +693,9 @@ public class FieldTable
             return null;
         }
 
-        private synchronized Map<String, AMQTypedValue> decode()
+        private synchronized Map<String, AMQTypedValue> decode(final int maxNestedObjects)
         {
+            validateEncodedForm(maxNestedObjects);
             final Map<String, AMQTypedValue> properties = new HashMap<>();
             final long encodedSize = getEncodedSize();
             if (encodedSize > 0)
@@ -574,7 +708,8 @@ public class FieldTable
                         final String key = AMQShortString.readAMQShortStringAsString(_encodedForm);
 
                         checkPropertyName(key);
-                        AMQTypedValue value = AMQTypedValue.readFromBuffer(_encodedForm);
+                        final AMQTypedValue value = AMQTypedValue.readFromBuffer(_encodedForm, maxNestedObjects);
+                        markValueValidated(value.getValue(), _validatedMaxNestedObjects);
                         properties.put(key, value);
                     }
                     while (_encodedForm.hasRemaining());
@@ -595,6 +730,15 @@ public class FieldTable
             }
             return properties;
         }
+
+        private synchronized void validateEncodedForm(final int maxNestedObjects)
+        {
+            if (_validatedMaxNestedObjects < 0 || maxNestedObjects < _validatedMaxNestedObjects)
+            {
+                FieldValueNestingValidator.validateTable(_encodedForm, maxNestedObjects);
+                markValidated(maxNestedObjects);
+            }
+        }
     }
 
     static class MapFieldTableSupport implements FieldTableSupport
@@ -615,7 +759,7 @@ public class FieldTable
         }
 
         @Override
-        public Object get(final String key)
+        public Object get(final String key, final int maxNestedObjects)
         {
             final AMQTypedValue value = _properties.get(key);
             if (value == null)
@@ -626,7 +770,7 @@ public class FieldTable
         }
 
         @Override
-        public boolean containsKey(final String key)
+        public boolean containsKey(final String key, final int maxNestedObjects)
         {
             return _properties.containsKey(key);
         }
@@ -651,7 +795,7 @@ public class FieldTable
         }
 
         @Override
-        public Map<String, AMQTypedValue> getAsMap()
+        public Map<String, AMQTypedValue> getAsMap(final int maxNestedObjects)
         {
             return _properties;
         }
@@ -663,7 +807,25 @@ public class FieldTable
         }
 
         @Override
-        public void validate()
+        public void validate(final int maxNestedObjects)
+        {
+            // noop
+        }
+
+        @Override
+        public boolean isDirect()
+        {
+            return false;
+        }
+
+        @Override
+        public void markValidated(final int maxNestedObjects)
+        {
+            // noop
+        }
+
+        @Override
+        public void validateNesting(final int maxNestedObjects)
         {
             // noop
         }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java
index b0a53dcdff..15af1c1aae 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java
@@ -40,6 +40,13 @@ public class FieldTableFactory
         return new FieldTable(qpidByteBuffer, length);
     }
 
+    static FieldTable createFieldTable(final QpidByteBuffer qpidByteBuffer,
+                                       final int length,
+                                       final int maxNestedObjects)
+    {
+        return new FieldTable(qpidByteBuffer, length, maxNestedObjects);
+    }
+
     public static FieldTable createFieldTable(final Map<String, Object> map)
     {
         return new FieldTable(map);
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldValueNestingValidator.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldValueNestingValidator.java
new file mode 100644
index 0000000000..42df6843f0
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldValueNestingValidator.java
@@ -0,0 +1,154 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+
+final class FieldValueNestingValidator
+{
+    private static final int MINIMUM_FIELD_NAME_ENCODING_SIZE = 2 * Byte.BYTES;
+    private static final int COMPOUND_VALUE_HEADER_SIZE = Byte.BYTES + Integer.BYTES;
+    private static final int FIRST_NESTED_VALUE_SIZE = MINIMUM_FIELD_NAME_ENCODING_SIZE + COMPOUND_VALUE_HEADER_SIZE;
+    private static final int SUBSEQUENT_NESTED_VALUE_SIZE = COMPOUND_VALUE_HEADER_SIZE;
+
+    private FieldValueNestingValidator()
+    {
+        // utility has no public constructor
+    }
+
+    static void validateTable(final QpidByteBuffer encodedTable, final int maxNestedObjects)
+    {
+        validateMaximum(maxNestedObjects);
+
+        try (final QpidByteBuffer table = encodedTable.duplicate())
+        {
+            checkDepth(1, maxNestedObjects);
+            validateTableContents(table, 1, maxNestedObjects);
+        }
+    }
+
+    static boolean mayExceedMaximumDepth(final int encodedSize, final int maxNestedObjects)
+    {
+        validateMaximum(maxNestedObjects);
+        if (maxNestedObjects == 0)
+        {
+            return true;
+        }
+
+        final long minimumExcessiveSize = FIRST_NESTED_VALUE_SIZE + (long) SUBSEQUENT_NESTED_VALUE_SIZE *
+                (maxNestedObjects - 1);
+        return encodedSize >= minimumExcessiveSize;
+    }
+
+    static void validateMaximum(final int maxNestedObjects)
+    {
+        if (maxNestedObjects < 0)
+        {
+            throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+        }
+    }
+
+    private static void validateTableContents(final QpidByteBuffer table,
+                                              final int depth,
+                                              final int maxNestedObjects)
+    {
+        while (table.hasRemaining())
+        {
+            validateFieldName(table);
+            validateValue(table, depth, maxNestedObjects);
+        }
+    }
+
+    private static void validateArrayContents(final QpidByteBuffer array,
+                                              final int depth,
+                                              final int maxNestedObjects)
+    {
+        while (array.hasRemaining())
+        {
+            validateValue(array, depth, maxNestedObjects);
+        }
+    }
+
+    private static void validateFieldName(final QpidByteBuffer table)
+    {
+        final int length = table.getUnsignedByte();
+        if (length == 0)
+        {
+            throw new IllegalArgumentException("Field-table property name must not be empty");
+        }
+
+        EncodingUtils.checkLength(length, table);
+        if (FieldTable._strictAMQP)
+        {
+            final byte[] bytes = new byte[length];
+            table.get(bytes);
+            FieldTable.checkPropertyName(new String(bytes, StandardCharsets.UTF_8));
+        }
+        else
+        {
+            table.position(table.position() + length);
+        }
+    }
+
+    private static void validateValue(final QpidByteBuffer buffer,
+                                      final int depth,
+                                      final int maxNestedObjects)
+    {
+        final AMQType type = AMQTypeMap.getType(buffer.get());
+        if (type == AMQType.FIELD_TABLE || type == AMQType.FIELD_ARRAY)
+        {
+            final int nestedDepth = depth + 1;
+            checkDepth(nestedDepth, maxNestedObjects);
+
+            final int length = EncodingUtils.checkLength(buffer.getUnsignedInt(), buffer);
+            try (final QpidByteBuffer nested = buffer.view(0, length))
+            {
+                buffer.position(buffer.position() + length);
+                if (type == AMQType.FIELD_TABLE)
+                {
+                    validateTableContents(nested, nestedDepth, maxNestedObjects);
+                }
+                else
+                {
+                    validateArrayContents(nested, nestedDepth, maxNestedObjects);
+                }
+            }
+        }
+        else if (type == AMQType.LONG_STRING || type == AMQType.ASCII_STRING || type == AMQType.WIDE_STRING)
+        {
+            EncodingUtils.readLongString(buffer);
+        }
+        else
+        {
+            type.skip(buffer);
+        }
+    }
+
+    private static void checkDepth(final int depth, final int maxNestedObjects)
+    {
+        if (depth > maxNestedObjects)
+        {
+            throw new AMQValueNestingException("Maximum field-value nesting depth (" + maxNestedObjects + ") exceeded");
+        }
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FrameBoundaryValidatingServerMethodProcessor.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FrameBoundaryValidatingServerMethodProcessor.java
new file mode 100644
index 0000000000..433fe4304d
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FrameBoundaryValidatingServerMethodProcessor.java
@@ -0,0 +1,610 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import java.util.Objects;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicContentHeaderProperties;
+import org.apache.qpid.server.protocol.v0_8.transport.ProtocolInitiation;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
+
+final class FrameBoundaryValidatingServerMethodProcessor
+        implements ServerMethodProcessor<ServerChannelMethodProcessor>, ServerChannelMethodProcessor
+{
+    private static final int NO_CHANNEL = -1;
+
+    private final ServerMethodProcessor<? extends ServerChannelMethodProcessor> _delegate;
+
+    private QpidByteBuffer _frameBody;
+    private int _channelId = NO_CHANNEL;
+    private ServerChannelMethodProcessor _channelDelegate;
+    private int _classId;
+    private int _methodId;
+    private boolean _currentMethodRecorded;
+    private boolean _currentMethodPublished;
+    private boolean _channelMethodValidationPerformed;
+    private boolean _channelMethodRejected;
+
+    FrameBoundaryValidatingServerMethodProcessor(final ServerMethodProcessor<? extends ServerChannelMethodProcessor> delegate)
+    {
+        _delegate = Objects.requireNonNull(delegate);
+    }
+
+    void begin(final QpidByteBuffer frameBody)
+    {
+        if (_frameBody != null)
+        {
+            throw new IllegalStateException("Method frame validation is already active");
+        }
+        _frameBody = Objects.requireNonNull(frameBody);
+        _channelId = NO_CHANNEL;
+        _channelDelegate = null;
+        _classId = 0;
+        _methodId = 0;
+        _currentMethodRecorded = false;
+        _currentMethodPublished = false;
+        _channelMethodValidationPerformed = false;
+        _channelMethodRejected = false;
+    }
+
+    void end()
+    {
+        _frameBody = null;
+        _channelId = NO_CHANNEL;
+        _channelDelegate = null;
+        _classId = 0;
+        _methodId = 0;
+        _currentMethodRecorded = false;
+        _currentMethodPublished = false;
+        _channelMethodValidationPerformed = false;
+        _channelMethodRejected = false;
+    }
+
+    void publishCurrentMethodIfFrameBodyConsumed()
+    {
+        ensureActive();
+        if (!_frameBody.hasRemaining())
+        {
+            publishCurrentMethod();
+        }
+    }
+
+    boolean isMethodBodyDecoded()
+    {
+        return _currentMethodPublished;
+    }
+
+    @Override
+    public ProtocolVersion getProtocolVersion()
+    {
+        try
+        {
+            return _delegate.getProtocolVersion();
+        }
+        catch (final ConnectionScopedRuntimeException | ServerScopedRuntimeException e)
+        {
+            throw e;
+        }
+        catch (RuntimeException e)
+        {
+            throw new ConnectionScopedRuntimeException(e);
+        }
+    }
+
+    @Override
+    public ServerChannelMethodProcessor getChannelMethodProcessor(final int channelId)
+    {
+        ensureActive();
+        if (_channelId == NO_CHANNEL)
+        {
+            _channelId = channelId;
+        }
+        else if (_channelId != channelId)
+        {
+            throw new IllegalStateException("Multiple channels referenced by one method frame");
+        }
+        return this;
+    }
+
+    @Override
+    public void receiveConnectionStartOk(final FieldTable clientProperties,
+                                         final AMQShortString mechanism,
+                                         final byte[] response,
+                                         final AMQShortString locale)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveConnectionStartOk(clientProperties, mechanism, response, locale);
+        }
+    }
+
+    @Override
+    public void receiveConnectionSecureOk(final byte[] response)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveConnectionSecureOk(response);
+        }
+    }
+
+    @Override
+    public void receiveConnectionTuneOk(final int channelMax, final long frameMax, final int heartbeat)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveConnectionTuneOk(channelMax, frameMax, heartbeat);
+        }
+    }
+
+    @Override
+    public void receiveConnectionOpen(final AMQShortString virtualHost,
+                                      final AMQShortString capabilities,
+                                      final boolean insist)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveConnectionOpen(virtualHost, capabilities, insist);
+        }
+    }
+
+    @Override
+    public void receiveChannelOpen(final int channelId)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveChannelOpen(channelId);
+        }
+    }
+
+    @Override
+    public void receiveConnectionClose(final int replyCode,
+                                       final AMQShortString replyText,
+                                       final int classId,
+                                       final int methodId)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveConnectionClose(replyCode, replyText, classId, methodId);
+        }
+    }
+
+    @Override
+    public void receiveConnectionCloseOk()
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveConnectionCloseOk();
+        }
+    }
+
+    @Override
+    public void receiveHeartbeat()
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveHeartbeat();
+        }
+    }
+
+    @Override
+    public void receiveProtocolHeader(final ProtocolInitiation protocolInitiation)
+    {
+        if (isFrameBodyConsumed())
+        {
+            _delegate.receiveProtocolHeader(protocolInitiation);
+        }
+    }
+
+    @Override
+    public void setCurrentMethod(final int classId, final int methodId)
+    {
+        ensureActive();
+        if (_currentMethodRecorded)
+        {
+            throw new IllegalStateException("Current method is already recorded for this frame");
+        }
+        _classId = classId;
+        _methodId = methodId;
+        _currentMethodRecorded = true;
+    }
+
+    @Override
+    public boolean ignoreAllButCloseOk()
+    {
+        if (_channelId == NO_CHANNEL)
+        {
+            return !isFrameBodyConsumed() || _delegate.ignoreAllButCloseOk();
+        }
+        return !isChannelMethodDispatchAllowed() || getChannelDelegate().ignoreAllButCloseOk();
+    }
+
+    @Override
+    public void receiveAccessRequest(final AMQShortString realm,
+                                     final boolean exclusive,
+                                     final boolean passive,
+                                     final boolean active,
+                                     final boolean write,
+                                     final boolean read)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveAccessRequest(realm, exclusive, passive, active, write, read);
+        }
+    }
+
+    @Override
+    public void receiveExchangeDeclare(final AMQShortString exchange,
+                                       final AMQShortString type,
+                                       final boolean passive,
+                                       final boolean durable,
+                                       final boolean autoDelete,
+                                       final boolean internal,
+                                       final boolean nowait,
+                                       final FieldTable arguments)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveExchangeDeclare(exchange, type, passive, durable, autoDelete, internal,
+                    nowait, arguments);
+        }
+    }
+
+    @Override
+    public void receiveExchangeDelete(final AMQShortString exchange, final boolean ifUnused, final boolean nowait)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveExchangeDelete(exchange, ifUnused, nowait);
+        }
+    }
+
+    @Override
+    public void receiveExchangeBound(final AMQShortString exchange,
+                                     final AMQShortString routingKey,
+                                     final AMQShortString queue)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveExchangeBound(exchange, routingKey, queue);
+        }
+    }
+
+    @Override
+    public void receiveQueueDeclare(final AMQShortString queue,
+                                    final boolean passive,
+                                    final boolean durable,
+                                    final boolean exclusive,
+                                    final boolean autoDelete,
+                                    final boolean nowait,
+                                    final FieldTable arguments)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveQueueDeclare(queue, passive, durable, exclusive, autoDelete, nowait, arguments);
+        }
+    }
+
+    @Override
+    public void receiveQueueBind(final AMQShortString queue,
+                                 final AMQShortString exchange,
+                                 final AMQShortString bindingKey,
+                                 final boolean nowait,
+                                 final FieldTable arguments)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveQueueBind(queue, exchange, bindingKey, nowait, arguments);
+        }
+    }
+
+    @Override
+    public void receiveQueuePurge(final AMQShortString queue, final boolean nowait)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveQueuePurge(queue, nowait);
+        }
+    }
+
+    @Override
+    public void receiveQueueDelete(final AMQShortString queue,
+                                   final boolean ifUnused,
+                                   final boolean ifEmpty,
+                                   final boolean nowait)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveQueueDelete(queue, ifUnused, ifEmpty, nowait);
+        }
+    }
+
+    @Override
+    public void receiveQueueUnbind(final AMQShortString queue,
+                                   final AMQShortString exchange,
+                                   final AMQShortString bindingKey,
+                                   final FieldTable arguments)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveQueueUnbind(queue, exchange, bindingKey, arguments);
+        }
+    }
+
+    @Override
+    public void receiveBasicRecover(final boolean requeue, final boolean sync)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicRecover(requeue, sync);
+        }
+    }
+
+    @Override
+    public void receiveBasicQos(final long prefetchSize, final int prefetchCount, final boolean global)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicQos(prefetchSize, prefetchCount, global);
+        }
+    }
+
+    @Override
+    public void receiveBasicConsume(final AMQShortString queue,
+                                    final AMQShortString consumerTag,
+                                    final boolean noLocal,
+                                    final boolean noAck,
+                                    final boolean exclusive,
+                                    final boolean nowait,
+                                    final FieldTable arguments)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicConsume(queue, consumerTag, noLocal, noAck, exclusive, nowait, arguments);
+        }
+    }
+
+    @Override
+    public void receiveBasicCancel(final AMQShortString consumerTag, final boolean noWait)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicCancel(consumerTag, noWait);
+        }
+    }
+
+    @Override
+    public void receiveBasicPublish(final AMQShortString exchange,
+                                    final AMQShortString routingKey,
+                                    final boolean mandatory,
+                                    final boolean immediate)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicPublish(exchange, routingKey, mandatory, immediate);
+        }
+    }
+
+    @Override
+    public void receiveBasicGet(final AMQShortString queue, final boolean noAck)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicGet(queue, noAck);
+        }
+    }
+
+    @Override
+    public void receiveBasicAck(final long deliveryTag, final boolean multiple)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicAck(deliveryTag, multiple);
+        }
+    }
+
+    @Override
+    public void receiveBasicReject(final long deliveryTag, final boolean requeue)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicReject(deliveryTag, requeue);
+        }
+    }
+
+    @Override
+    public void receiveTxSelect()
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveTxSelect();
+        }
+    }
+
+    @Override
+    public void receiveTxCommit()
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveTxCommit();
+        }
+    }
+
+    @Override
+    public void receiveTxRollback()
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveTxRollback();
+        }
+    }
+
+    @Override
+    public void receiveConfirmSelect(final boolean nowait)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveConfirmSelect(nowait);
+        }
+    }
+
+    @Override
+    public void receiveChannelFlow(final boolean active)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveChannelFlow(active);
+        }
+    }
+
+    @Override
+    public void receiveChannelFlowOk(final boolean active)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveChannelFlowOk(active);
+        }
+    }
+
+    @Override
+    public void receiveChannelClose(final int replyCode,
+                                    final AMQShortString replyText,
+                                    final int classId,
+                                    final int methodId)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveChannelClose(replyCode, replyText, classId, methodId);
+        }
+    }
+
+    @Override
+    public void receiveChannelCloseOk()
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveChannelCloseOk();
+        }
+    }
+
+    @Override
+    public void receiveMessageContent(final QpidByteBuffer data)
+    {
+        if (isFrameBodyConsumed())
+        {
+            getChannelDelegate().receiveMessageContent(data);
+        }
+    }
+
+    @Override
+    public void receiveMessageHeader(final BasicContentHeaderProperties properties, final long bodySize)
+    {
+        boolean propertiesTransferred = false;
+        try
+        {
+            if (isFrameBodyConsumed())
+            {
+                final ServerChannelMethodProcessor channelDelegate = getChannelDelegate();
+                propertiesTransferred = true;
+                channelDelegate.receiveMessageHeader(properties, bodySize);
+            }
+        }
+        finally
+        {
+            if (!propertiesTransferred)
+            {
+                properties.dispose();
+            }
+        }
+    }
+
+    @Override
+    public void receiveBasicNack(final long deliveryTag, final boolean multiple, final boolean requeue)
+    {
+        if (isChannelMethodDispatchAllowed())
+        {
+            getChannelDelegate().receiveBasicNack(deliveryTag, multiple, requeue);
+        }
+    }
+
+    private boolean isFrameBodyConsumed()
+    {
+        ensureActive();
+        if (_frameBody.hasRemaining())
+        {
+            return false;
+        }
+        publishCurrentMethod();
+        return true;
+    }
+
+    private boolean isChannelMethodDispatchAllowed()
+    {
+        if (!isFrameBodyConsumed())
+        {
+            return false;
+        }
+        if (!_channelMethodValidationPerformed)
+        {
+            _channelMethodRejected = getChannelDelegate().rejectMethodFrameIfContentIncomplete();
+            _channelMethodValidationPerformed = true;
+        }
+        return !_channelMethodRejected;
+    }
+
+    private void publishCurrentMethod()
+    {
+        if (!_currentMethodRecorded)
+        {
+            throw new IllegalStateException("No current method was recorded for this frame");
+        }
+        if (!_currentMethodPublished)
+        {
+            _currentMethodPublished = true;
+            _delegate.setCurrentMethod(_classId, _methodId);
+        }
+    }
+
+    private void ensureActive()
+    {
+        if (_frameBody == null)
+        {
+            throw new IllegalStateException("Method frame validation is not active");
+        }
+    }
+
+    private ServerChannelMethodProcessor getChannelDelegate()
+    {
+        if (_channelId == NO_CHANNEL)
+        {
+            throw new IllegalStateException("No channel was selected for the method frame");
+        }
+        if (_channelDelegate == null)
+        {
+            _channelDelegate = _delegate.getChannelMethodProcessor(_channelId);
+        }
+        return _channelDelegate;
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java
index fc6d831bf9..590c0701f2 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java
@@ -23,19 +23,20 @@ package org.apache.qpid.server.protocol.v0_8;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.message.MessageDestination;
 import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
 import org.apache.qpid.server.protocol.v0_8.transport.MessagePublishInfo;
-import org.apache.qpid.server.message.MessageDestination;
 
 public class IncomingMessage
 {
 
     private final MessagePublishInfo _messagePublishInfo;
-    private final List<ContentBody> _contentChunks = new ArrayList<>();
+    private final List<QpidByteBuffer> _contentChunks = new ArrayList<>();
 
     private ContentHeaderBody _contentHeaderBody;
     private MessageDestination _messageDestination;
+    private int _contentBodyFrameCount;
 
     /**
      * Keeps a track of how many bytes we have received in body frames
@@ -57,11 +58,21 @@ public class IncomingMessage
         return _messagePublishInfo;
     }
 
-    public long addContentBodyFrame(final ContentBody contentChunk)
+    public boolean addContentBodyFrame(final QpidByteBuffer contentChunk)
     {
-        _bodyLengthReceived += contentChunk.getSize();
-        _contentChunks.add(contentChunk);
-        return _bodyLengthReceived;
+        final int contentSize = contentChunk.remaining();
+        if (contentSize > getSize() - _bodyLengthReceived)
+        {
+            return false;
+        }
+
+        if (contentSize > 0)
+        {
+            _contentChunks.add(contentChunk.duplicate());
+        }
+        _bodyLengthReceived += contentSize;
+        _contentBodyFrameCount++;
+        return true;
     }
 
     public boolean allContentReceived()
@@ -94,14 +105,35 @@ public class IncomingMessage
         _messageDestination = e;
     }
 
-    public int getBodyCount()
+    public int getContentBodyFrameCount()
+    {
+        return _contentBodyFrameCount;
+    }
+
+    public int getContentChunkCount()
     {
         return _contentChunks.size();
     }
 
-    public ContentBody getContentChunk(int index)
+    public QpidByteBuffer getContentChunk(final int index)
     {
         return _contentChunks.get(index);
     }
 
+    public void dispose()
+    {
+        final ContentHeaderBody contentHeaderBody = _contentHeaderBody;
+        _contentHeaderBody = null;
+        if (contentHeaderBody != null)
+        {
+            contentHeaderBody.dispose();
+        }
+
+        for (final QpidByteBuffer contentChunk : _contentChunks)
+        {
+            contentChunk.dispose();
+        }
+        _contentChunks.clear();
+    }
+
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java
index 59d498572e..68bb87a91e 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java
@@ -23,11 +23,46 @@ package org.apache.qpid.server.protocol.v0_8;
 import java.io.IOException;
 
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
 import org.apache.qpid.server.protocol.ProtocolVersion;
-import org.apache.qpid.server.protocol.v0_8.transport.*;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQProtocolVersionException;
+import org.apache.qpid.server.protocol.v0_8.transport.AccessRequestBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicAckBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicCancelBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicConsumeBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicGetBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicNackBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicPublishBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicRecoverBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicRecoverSyncBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicRejectBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelFlowBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelFlowOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConfirmSelectBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionOpenBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionSecureOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeBoundBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeDeleteBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueBindBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeleteBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueuePurgeBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueUnbindBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
 
 public class ServerDecoder extends AMQDecoder<ServerMethodProcessor<? extends ServerChannelMethodProcessor>>
 {
+    private final FrameBoundaryValidatingServerMethodProcessor _validatingMethodProcessor;
 
     /**
      * Creates a new AMQP decoder.
@@ -36,218 +71,296 @@ public class ServerDecoder extends AMQDecoder<ServerMethodProcessor<? extends Se
      */
     public ServerDecoder(final ServerMethodProcessor<? extends ServerChannelMethodProcessor> methodProcessor)
     {
-        super(true, methodProcessor);
+        this(methodProcessor, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
     }
 
-    public void decodeBuffer(QpidByteBuffer buf) throws AMQFrameDecodingException, AMQProtocolVersionException, IOException
+    ServerDecoder(final ServerMethodProcessor<? extends ServerChannelMethodProcessor> methodProcessor,
+                  final int maxNestedObjects)
+    {
+        super(true, methodProcessor, maxNestedObjects);
+        _validatingMethodProcessor = new FrameBoundaryValidatingServerMethodProcessor(methodProcessor);
+    }
+
+    public void decodeBuffer(final QpidByteBuffer buf)
+            throws AMQFrameDecodingException, AMQProtocolVersionException, IOException
     {
         decode(buf);
     }
 
 
     @Override
-    protected void processMethod(int channelId,
-                       QpidByteBuffer in)
+    protected void processMethod(final int channelId,
+                                 final QpidByteBuffer in)
             throws AMQFrameDecodingException
     {
-        ServerMethodProcessor<? extends ServerChannelMethodProcessor> methodProcessor = getMethodProcessor();
-        final int classAndMethod = in.getInt();
-        int classId = classAndMethod >> 16;
-        int methodId = classAndMethod & 0xFFFF;
-        methodProcessor.setCurrentMethod(classId, methodId);
+        final ServerMethodProcessor<? extends ServerChannelMethodProcessor> actualMethodProcessor =
+                getMethodProcessor();
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = _validatingMethodProcessor;
+        _validatingMethodProcessor.begin(in);
         try
         {
-            switch (classAndMethod)
+            if (in.remaining() < Integer.BYTES)
+            {
+                throw new AMQFrameDecodingException("Method frame does not contain a class and method identifier");
+            }
+            final int classAndMethod = in.getInt();
+            final int classId = classAndMethod >> 16;
+            final int methodId = classAndMethod & 0xFFFF;
+            try
             {
-                //CONNECTION_CLASS:
-                case 0x000a000b:
-                    ConnectionStartOkBody.process(in, methodProcessor);
-                    break;
-                case 0x000a0015:
-                    ConnectionSecureOkBody.process(in, methodProcessor);
-                    break;
-                case 0x000a001f:
-                    ConnectionTuneOkBody.process(in, methodProcessor);
-                    break;
-                case 0x000a0028:
-                    ConnectionOpenBody.process(in, methodProcessor);
-                    break;
-                case 0x000a0032:
-                    if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
-                    {
-                        throw newUnknownMethodException(classId, methodId,
-                                                        methodProcessor.getProtocolVersion());
-                    }
-                    else
-                    {
-                        ConnectionCloseBody.process(in, methodProcessor);
-                    }
-                    break;
-                case 0x000a0033:
-                    if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
-                    {
-                        throw newUnknownMethodException(classId, methodId,
-                                                        methodProcessor.getProtocolVersion());
-                    }
-                    else
-                    {
-                        methodProcessor.receiveConnectionCloseOk();
-                    }
-                    break;
-                case 0x000a003c:
-                    if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
-                    {
-                        ConnectionCloseBody.process(in, methodProcessor);
-                    }
-                    else
-                    {
-                        throw newUnknownMethodException(classId, methodId,
-                                                        methodProcessor.getProtocolVersion());
-                    }
-                    break;
-                case 0x000a003d:
-                    if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
-                    {
-                        methodProcessor.receiveConnectionCloseOk();
-                    }
-                    else
-                    {
-                        throw newUnknownMethodException(classId, methodId,
-                                                        methodProcessor.getProtocolVersion());
-                    }
-                    break;
-
-                // CHANNEL_CLASS:
-
-                case 0x0014000a:
-                    ChannelOpenBody.process(channelId, in, methodProcessor);
-                    break;
-                case 0x00140014:
-                    ChannelFlowBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00140015:
-                    ChannelFlowOkBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00140028:
-                    ChannelCloseBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00140029:
-                    methodProcessor.getChannelMethodProcessor(channelId).receiveChannelCloseOk();
-                    break;
-
-                // ACCESS_CLASS:
-
-                case 0x001e000a:
-                    AccessRequestBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-
-                // EXCHANGE_CLASS:
-
-                case 0x0028000a:
-                    ExchangeDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00280014:
-                    ExchangeDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00280016:
-                    ExchangeBoundBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-
-
-                // QUEUE_CLASS:
-
-                case 0x0032000a:
-                    QueueDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00320014:
-                    QueueBindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x0032001e:
-                    QueuePurgeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00320028:
-                    QueueDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x00320032:
-                    QueueUnbindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-
-
-                // BASIC_CLASS:
-
-                case 0x003c000a:
-                    BasicQosBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0014:
-                    BasicConsumeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c001e:
-                    BasicCancelBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0028:
-                    BasicPublishBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0046:
-                    BasicGetBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0050:
-                    BasicAckBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c005a:
-                    BasicRejectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0064:
-                    BasicRecoverBody.process(in, methodProcessor.getProtocolVersion(),
-                                             methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0066:
-                    BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c006e:
-                    BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-                case 0x003c0078:
-                    BasicNackBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-
-                // CONFIRM CLASS:
-
-                case 0x0055000a:
-                    ConfirmSelectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
-                    break;
-
-                // TX_CLASS:
-
-                case 0x005a000a:
-                    if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
-                    {
-                        methodProcessor.getChannelMethodProcessor(channelId).receiveTxSelect();
-                    }
-                    break;
-                case 0x005a0014:
-                    if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
-                    {
-                        methodProcessor.getChannelMethodProcessor(channelId).receiveTxCommit();
-                    }
-                    break;
-                case 0x005a001e:
-                    if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
-                    {
-                        methodProcessor.getChannelMethodProcessor(channelId).receiveTxRollback();
-                    }
-                    break;
-
-
-                default:
-                    throw newUnknownMethodException(classId, methodId,
-                                                    methodProcessor.getProtocolVersion());
+                if (classId == ConnectionStartOkBody.CLASS_ID && channelId != 0)
+                {
+                    throw new AMQFrameDecodingException(ErrorCodes.COMMAND_INVALID, "Connection class method " +
+                            methodId + " must use channel 0, received channel " + channelId, null);
+                }
+                methodProcessor.setCurrentMethod(classId, methodId);
+                switch (classAndMethod)
+                {
+                    //CONNECTION_CLASS:
+                    case 0x000a000b:
+                        ConnectionStartOkBody.process(in, methodProcessor, getMaxNestedObjects());
+                        break;
+                    case 0x000a0015:
+                        ConnectionSecureOkBody.process(in, methodProcessor);
+                        break;
+                    case 0x000a001f:
+                        ConnectionTuneOkBody.process(in, methodProcessor);
+                        break;
+                    case 0x000a0028:
+                        ConnectionOpenBody.process(in, methodProcessor);
+                        break;
+                    case 0x000a0032:
+                        if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+                        {
+                            throw newValidatedUnknownMethodException(classId, methodId,
+                                    methodProcessor.getProtocolVersion());
+                        }
+                        else
+                        {
+                            ConnectionCloseBody.process(in, methodProcessor);
+                        }
+                        break;
+                    case 0x000a0033:
+                        if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+                        {
+                            throw newValidatedUnknownMethodException(classId, methodId,
+                                    methodProcessor.getProtocolVersion());
+                        }
+                        else
+                        {
+                            methodProcessor.receiveConnectionCloseOk();
+                        }
+                        break;
+                    case 0x000a003c:
+                        if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+                        {
+                            ConnectionCloseBody.process(in, methodProcessor);
+                        }
+                        else
+                        {
+                            throw newValidatedUnknownMethodException(classId, methodId,
+                                    methodProcessor.getProtocolVersion());
+                        }
+                        break;
+                    case 0x000a003d:
+                        if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+                        {
+                            methodProcessor.receiveConnectionCloseOk();
+                        }
+                        else
+                        {
+                            throw newValidatedUnknownMethodException(classId, methodId,
+                                    methodProcessor.getProtocolVersion());
+                        }
+                        break;
+
+                    // CHANNEL_CLASS:
+
+                    case 0x0014000a:
+                        ChannelOpenBody.process(channelId, in, methodProcessor);
+                        break;
+                    case 0x00140014:
+                        ChannelFlowBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x00140015:
+                        ChannelFlowOkBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x00140028:
+                        ChannelCloseBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x00140029:
+                        methodProcessor.getChannelMethodProcessor(channelId).receiveChannelCloseOk();
+                        break;
+
+                    // ACCESS_CLASS:
+
+                    case 0x001e000a:
+                        AccessRequestBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+
+                    // EXCHANGE_CLASS:
+
+                    case 0x0028000a:
+                        ExchangeDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+                                getMaxNestedObjects());
+                        break;
+                    case 0x00280014:
+                        ExchangeDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x00280016:
+                        ExchangeBoundBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+
+
+                    // QUEUE_CLASS:
+
+                    case 0x0032000a:
+                        QueueDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+                                getMaxNestedObjects());
+                        break;
+                    case 0x00320014:
+                        QueueBindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+                                getMaxNestedObjects());
+                        break;
+                    case 0x0032001e:
+                        QueuePurgeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x00320028:
+                        QueueDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x00320032:
+                        QueueUnbindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+                                getMaxNestedObjects());
+                        break;
+
+
+                    // BASIC_CLASS:
+
+                    case 0x003c000a:
+                        BasicQosBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0014:
+                        BasicConsumeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+                                getMaxNestedObjects());
+                        break;
+                    case 0x003c001e:
+                        BasicCancelBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0028:
+                        BasicPublishBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0046:
+                        BasicGetBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0050:
+                        BasicAckBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c005a:
+                        BasicRejectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0064:
+                        BasicRecoverBody.process(in, methodProcessor.getProtocolVersion(),
+                                methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0066:
+                        BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c006e:
+                        BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+                    case 0x003c0078:
+                        BasicNackBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+
+                    // CONFIRM CLASS:
+
+                    case 0x0055000a:
+                        ConfirmSelectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+                        break;
+
+                    // TX_CLASS:
 
+                    case 0x005a000a:
+                        if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
+                        {
+                            methodProcessor.getChannelMethodProcessor(channelId).receiveTxSelect();
+                        }
+                        break;
+                    case 0x005a0014:
+                        if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
+                        {
+                            methodProcessor.getChannelMethodProcessor(channelId).receiveTxCommit();
+                        }
+                        break;
+                    case 0x005a001e:
+                        if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
+                        {
+                            methodProcessor.getChannelMethodProcessor(channelId).receiveTxRollback();
+                        }
+                        break;
+
+
+                    default:
+                        throw newValidatedUnknownMethodException(classId, methodId,
+                                methodProcessor.getProtocolVersion());
+                }
+
+                if (in.hasRemaining())
+                {
+                    throw new AMQFrameDecodingException("Method frame body was not fully consumed: classId=" +
+                            classId + ", methodId=" + methodId + ", remaining=" + in.remaining());
+                }
+            }
+            catch (AMQValueNestingException e)
+            {
+                if (isMethodProcessorFailure())
+                {
+                    throw new ConnectionScopedRuntimeException(e);
+                }
+                throw AMQFrameDecodingException.forDecodingFailure("Could not decode method " + classId + ':' +
+                        methodId, classId, methodId, e);
+            }
+            catch (AMQFrameDecodingException e)
+            {
+                throw new AMQFrameDecodingException(e.getErrorCode(), e.getMessage(), classId, methodId, e);
+            }
+            catch (final ConnectionScopedRuntimeException | ServerScopedRuntimeException e)
+            {
+                throw e;
+            }
+            catch (final RuntimeException e)
+            {
+                if (isMethodProcessorFailure())
+                {
+                    throw e;
+                }
+                throw AMQFrameDecodingException.forDecodingFailure("Could not decode method " + classId + ':' +
+                        methodId, classId, methodId, e);
             }
         }
         finally
         {
-            methodProcessor.setCurrentMethod(0, 0);
+            try
+            {
+                actualMethodProcessor.setCurrentMethod(0, 0);
+            }
+            finally
+            {
+                _validatingMethodProcessor.end();
+            }
         }
     }
 
+    private boolean isMethodProcessorFailure()
+    {
+        return _validatingMethodProcessor.isMethodBodyDecoded();
+    }
+
+    private AMQFrameDecodingException newValidatedUnknownMethodException(final int classId,
+                                                                         final int methodId,
+                                                                         final ProtocolVersion protocolVersion)
+    {
+        _validatingMethodProcessor.publishCurrentMethodIfFrameBodyConsumed();
+        return newUnknownMethodException(classId, methodId, protocolVersion);
+    }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java
index a8dfa27b4f..f398a73309 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -192,6 +193,14 @@ public class BasicConsumeBody extends AMQMethodBodyImpl implements EncodableAMQD
                                final ServerChannelMethodProcessor dispatcher)
             throws AMQFrameDecodingException
     {
+        process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static void process(final QpidByteBuffer buffer,
+                               final ServerChannelMethodProcessor dispatcher,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
 
         int ticket = buffer.getUnsignedShort();
         AMQShortString queue = AMQShortString.readAMQShortString(buffer);
@@ -202,20 +211,21 @@ public class BasicConsumeBody extends AMQMethodBodyImpl implements EncodableAMQD
         boolean noAck = (bitfield & 0x02) == 0x02;
         boolean exclusive = (bitfield & 0x04) == 0x04;
         boolean nowait = (bitfield & 0x08) == 0x08;
-        FieldTable arguments = EncodingUtils.readFieldTable(buffer);
-        if (!dispatcher.ignoreAllButCloseOk())
+        final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+        try
         {
-            dispatcher.receiveBasicConsume(queue,
-                                           consumerTag,
-                                           noLocal,
-                                           noAck,
-                                           exclusive,
-                                           nowait,
-                                           FieldTable.convertToDecodedFieldTable(arguments));
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                dispatcher.receiveBasicConsume(queue, consumerTag, noLocal, noAck, exclusive, nowait,
+                                               FieldTable.convertToDecodedFieldTable(arguments));
+            }
         }
-        if (arguments != null)
+        finally
         {
-            arguments.dispose();
+            if (arguments != null)
+            {
+                arguments.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java
index d07132f800..514a9e9a5c 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java
@@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
 
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -75,12 +76,10 @@ public class BasicContentHeaderProperties
     private int _propertyFlags = 0;
     private static final int CONTENT_TYPE_MASK = 1 << 15;
     private static final int ENCODING_MASK = 1 << 14;
-    private static final int HEADERS_MASK = 1 << 13;
     private static final int DELIVERY_MODE_MASK = 1 << 12;
     private static final int PRIORITY_MASK = 1 << 11;
     private static final int CORRELATION_ID_MASK = 1 << 10;
     private static final int REPLY_TO_MASK = 1 << 9;
-    private static final int EXPIRATION_MASK = 1 << 8;
     private static final int MESSAGE_ID_MASK = 1 << 7;
     private static final int TIMESTAMP_MASK = 1 << 6;
     private static final int TYPE_MASK = 1 << 5;
@@ -88,11 +87,15 @@ public class BasicContentHeaderProperties
     private static final int APPLICATION_ID_MASK = 1 << 3;
     private static final int CLUSTER_ID_MASK = 1 << 2;
 
+    private final int _maxNestedObjects;
     private volatile QpidByteBuffer _encodedForm;
 
+    static final int HEADERS_MASK = 1 << 13;
+    static final int EXPIRATION_MASK = 1 << 8;
 
-    public BasicContentHeaderProperties(BasicContentHeaderProperties other)
+    public BasicContentHeaderProperties(final BasicContentHeaderProperties other)
     {
+        _maxNestedObjects = other._maxNestedObjects;
         _headers = FieldTableFactory.createFieldTable(other.getHeadersAsMap());
 
         _contentType = other._contentType;
@@ -125,7 +128,8 @@ public class BasicContentHeaderProperties
     }
 
     public BasicContentHeaderProperties()
-    { 
+    {
+        _maxNestedObjects = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS;
     }
 
     public synchronized int getPropertyListSize()
@@ -346,10 +350,36 @@ public class BasicContentHeaderProperties
 
     }
 
-    public BasicContentHeaderProperties(QpidByteBuffer buffer, int propertyFlags, int size) throws AMQFrameDecodingException
+    public BasicContentHeaderProperties(final QpidByteBuffer buffer,
+                                        final int propertyFlags,
+                                        final int size)
+            throws AMQFrameDecodingException
+    {
+        this(buffer, propertyFlags, size, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    BasicContentHeaderProperties(final QpidByteBuffer buffer,
+                                 final int propertyFlags,
+                                 final int size,
+                                 final int maxNestedObjects)
+            throws AMQFrameDecodingException
     {
+        if (maxNestedObjects < 0)
+        {
+            throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+        }
+        _maxNestedObjects = maxNestedObjects;
         _propertyFlags = propertyFlags;
 
+        final int supportedPropertyFlags = CONTENT_TYPE_MASK | ENCODING_MASK | HEADERS_MASK | DELIVERY_MODE_MASK |
+                PRIORITY_MASK | CORRELATION_ID_MASK | REPLY_TO_MASK | EXPIRATION_MASK | MESSAGE_ID_MASK |
+                TIMESTAMP_MASK | TYPE_MASK | USER_ID_MASK | APPLICATION_ID_MASK | CLUSTER_ID_MASK;
+        if ((_propertyFlags & ~supportedPropertyFlags) != 0)
+        {
+            throw new AMQFrameDecodingException("Unsupported content header property flags: 0x" +
+                    Integer.toHexString(_propertyFlags));
+        }
+
         if (LOGGER.isDebugEnabled())
         {
             LOGGER.debug("Property flags: " + _propertyFlags);
@@ -359,6 +389,16 @@ public class BasicContentHeaderProperties
         try (QpidByteBuffer byteBuffer = _encodedForm.slice())
         {
             decode(byteBuffer);
+            if (byteBuffer.hasRemaining())
+            {
+                throw new AMQFrameDecodingException("Content header property list was not fully consumed: remaining=" +
+                        byteBuffer.remaining());
+            }
+        }
+        catch (AMQFrameDecodingException | RuntimeException e)
+        {
+            dispose();
+            throw e;
         }
         buffer.position(buffer.position()+size);
 
@@ -378,7 +418,7 @@ public class BasicContentHeaderProperties
 
         if ((_propertyFlags & HEADERS_MASK) != 0)
         {
-            _headers = EncodingUtils.readFieldTable(buffer);
+            _headers = EncodingUtils.readFieldTable(buffer, _maxNestedObjects);
         }
 
         if ((_propertyFlags & DELIVERY_MODE_MASK) != 0)
@@ -516,7 +556,7 @@ public class BasicContentHeaderProperties
 
     public synchronized Map<String, Object> getHeadersAsMap()
     {
-        return FieldTable.convertToMap(_headers);
+        return FieldTable.convertToMap(_headers, _maxNestedObjects);
     }
 
     public synchronized void setHeaders(FieldTable headers)
@@ -880,7 +920,7 @@ public class BasicContentHeaderProperties
 
             if ((_propertyFlags & HEADERS_MASK) != 0)
             {
-                _headers = EncodingUtils.readFieldTable(byteBuffer);
+                _headers = EncodingUtils.readFieldTable(byteBuffer, _maxNestedObjects);
             }
         }
     }
@@ -889,7 +929,7 @@ public class BasicContentHeaderProperties
     {
         if (_headers != null)
         {
-            _headers.validate();
+            _headers.validate(_maxNestedObjects);
         }
     }
 
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java
index 0964d0e59e..a8946d39bb 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java
@@ -128,13 +128,19 @@ public class ChannelAlertBody extends AMQMethodBodyImpl implements EncodableAMQD
         int replyCode = buffer.getUnsignedShort();
         AMQShortString replyText = AMQShortString.readAMQShortString(buffer);
         FieldTable details = EncodingUtils.readFieldTable(buffer);
-        if(!dispatcher.ignoreAllButCloseOk())
+        try
         {
-            dispatcher.receiveChannelAlert(replyCode, replyText, FieldTable.convertToDecodedFieldTable(details));
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                dispatcher.receiveChannelAlert(replyCode, replyText, FieldTable.convertToDecodedFieldTable(details));
+            }
         }
-        if (details != null)
+        finally
         {
-            details.dispose();
+            if (details != null)
+            {
+                details.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java
index a5ad7721d0..bfca7b03b7 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java
@@ -147,20 +147,23 @@ public class ConnectionStartBody extends AMQMethodBodyImpl implements EncodableA
         short versionMajor = in.getUnsignedByte();
         short versionMinor = in.getUnsignedByte();
         FieldTable serverProperties = EncodingUtils.readFieldTable(in);
-        byte[] mechanisms = EncodingUtils.readBytes(in);
-        byte[] locales = EncodingUtils.readBytes(in);
-
-        if(!dispatcher.ignoreAllButCloseOk())
+        try
         {
-            dispatcher.receiveConnectionStart(versionMajor,
-                                              versionMinor,
-                                              FieldTable.convertToDecodedFieldTable(serverProperties),
-                                              mechanisms,
-                                              locales);
+            final byte[] mechanisms = EncodingUtils.readBytes(in);
+            final byte[] locales = EncodingUtils.readBytes(in);
+
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                dispatcher.receiveConnectionStart(versionMajor, versionMinor,
+                        FieldTable.convertToDecodedFieldTable(serverProperties), mechanisms, locales);
+            }
         }
-        if (serverProperties != null)
+        finally
         {
-            serverProperties.dispose();
+            if (serverProperties != null)
+            {
+                serverProperties.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java
index 2a6b7fbae8..f5335b8328 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -136,21 +137,33 @@ public class ConnectionStartOkBody extends AMQMethodBodyImpl implements Encodabl
     public static void process(final QpidByteBuffer in, final ServerMethodProcessor dispatcher)
             throws AMQFrameDecodingException
     {
+        process(in, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static void process(final QpidByteBuffer in,
+                               final ServerMethodProcessor dispatcher,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
 
-        FieldTable clientProperties = EncodingUtils.readFieldTable(in);
-        AMQShortString mechanism = AMQShortString.readAMQShortString(in);
-        byte[] response = EncodingUtils.readBytes(in);
-        AMQShortString locale = AMQShortString.readAMQShortString(in);
-        if (!dispatcher.ignoreAllButCloseOk())
+        final FieldTable clientProperties = EncodingUtils.readFieldTable(in, maxNestedObjects);
+        try
         {
-            dispatcher.receiveConnectionStartOk(FieldTable.convertToDecodedFieldTable(clientProperties),
-                                                mechanism,
-                                                response,
-                                                locale);
+            final AMQShortString mechanism = AMQShortString.readAMQShortString(in);
+            final byte[] response = EncodingUtils.readBytes(in);
+            final AMQShortString locale = AMQShortString.readAMQShortString(in);
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                dispatcher.receiveConnectionStartOk(FieldTable.convertToDecodedFieldTable(clientProperties),
+                        mechanism, response, locale);
+            }
         }
-        if (clientProperties != null)
+        finally
         {
-            clientProperties.dispose();
+            if (clientProperties != null)
+            {
+                clientProperties.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java
index 6adbce5571..85da68a17a 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java
@@ -20,9 +20,14 @@
  */
 package org.apache.qpid.server.protocol.v0_8.transport;
 
+import java.nio.BufferUnderflowException;
+
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.transport.ByteBufferSender;
 
 public class ContentHeaderBody implements AMQBody
@@ -165,29 +170,94 @@ public class ContentHeaderBody implements AMQBody
     }
 
     public static void process(final QpidByteBuffer buffer,
-                               final ChannelMethodProcessor methodProcessor, final long size)
+                               final MethodProcessor<? extends ChannelMethodProcessor> methodProcessor,
+                               final int channelId,
+                               final long size)
             throws AMQFrameDecodingException
     {
-        int classId = buffer.getUnsignedShort();
-        buffer.getUnsignedShort();
-        long bodySize = buffer.getLong();
-        int propertyFlags = buffer.getUnsignedShort();
-
-        BasicContentHeaderProperties properties;
+        process(buffer, methodProcessor, channelId, size, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
 
+    public static void process(final QpidByteBuffer buffer,
+                               final MethodProcessor<? extends ChannelMethodProcessor> methodProcessor,
+                               final int channelId,
+                               final long size,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
+        final int classId;
+        final int weight;
+        final long bodySize;
+        final int propertyFlags;
+        try
+        {
+            classId = buffer.getUnsignedShort();
+            weight = buffer.getUnsignedShort();
+            bodySize = buffer.getLong();
+            propertyFlags = buffer.getUnsignedShort();
+        }
+        catch (BufferUnderflowException | IllegalArgumentException | IllegalStateException e)
+        {
+            throw AMQFrameDecodingException.forDecodingFailure("Could not decode content header", e);
+        }
         if (classId != CLASS_ID)
         {
             throw new AMQFrameDecodingException("Unsupported content header class id: " + classId, null);
         }
-        properties = new BasicContentHeaderProperties(buffer, propertyFlags, (int)(size-14));
 
-        if(!methodProcessor.ignoreAllButCloseOk())
+        validateWeight(weight, methodProcessor);
+
+        if (bodySize < 0)
         {
-            methodProcessor.receiveMessageHeader(properties, bodySize);
+            buffer.position(buffer.limit());
+            methodProcessor.receiveOversizedMessageHeader(channelId, bodySize);
+            return;
         }
-        else
+
+        final BasicContentHeaderProperties properties;
+        try
+        {
+            properties = new BasicContentHeaderProperties(buffer, propertyFlags, (int) (size - HEADER_SIZE),
+                    maxNestedObjects);
+        }
+        catch (BufferUnderflowException | IllegalArgumentException | IllegalStateException e)
+        {
+            throw AMQFrameDecodingException.forDecodingFailure("Could not decode content header properties", e);
+        }
+        boolean propertiesTransferred = false;
+        try
+        {
+            final ChannelMethodProcessor channelMethodProcessor = methodProcessor.getChannelMethodProcessor(channelId);
+
+            if (!channelMethodProcessor.ignoreAllButCloseOk())
+            {
+                propertiesTransferred = true;
+                channelMethodProcessor.receiveMessageHeader(properties, bodySize);
+            }
+        }
+        finally
+        {
+            if (!propertiesTransferred)
+            {
+                properties.dispose();
+            }
+        }
+    }
+
+    private static void validateWeight(final int weight,
+                                       final MethodProcessor<? extends ChannelMethodProcessor> methodProcessor)
+            throws AMQFrameDecodingException
+    {
+        if (weight != 0)
         {
-            properties.dispose();
+            // AMQP 0-8 uses weight for structured content; later versions require the field to be zero
+            final ProtocolVersion protocolVersion = methodProcessor.getProtocolVersion();
+            final boolean structuredContent = ProtocolVersion.v0_8.equals(protocolVersion);
+            final int errorCode = structuredContent ? ErrorCodes.NOT_IMPLEMENTED : ErrorCodes.FRAME_ERROR;
+            final String message = structuredContent
+                    ? "AMQP 0-8 structured content is not supported (content weight " + weight + ")"
+                    : "Content weight must be zero for AMQP " + protocolVersion + " (received " + weight + ")";
+            throw new AMQFrameDecodingException(errorCode, message, null);
         }
     }
 
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java
index 8f3265d2ce..252d72e74b 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -204,6 +205,14 @@ public class ExchangeDeclareBody extends AMQMethodBodyImpl implements EncodableA
     public static void process(final QpidByteBuffer buffer,
                                final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
     {
+        process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static void process(final QpidByteBuffer buffer,
+                               final ServerChannelMethodProcessor dispatcher,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
 
         int ticket = buffer.getUnsignedShort();
         AMQShortString exchange = AMQShortString.readAMQShortString(buffer);
@@ -214,21 +223,21 @@ public class ExchangeDeclareBody extends AMQMethodBodyImpl implements EncodableA
         boolean autoDelete = (bitfield & 0x4) == 0x4;
         boolean internal = (bitfield & 0x8) == 0x8;
         boolean nowait = (bitfield & 0x10) == 0x10;
-        FieldTable arguments = EncodingUtils.readFieldTable(buffer);
-        if(!dispatcher.ignoreAllButCloseOk())
+        final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+        try
         {
-            dispatcher.receiveExchangeDeclare(exchange,
-                                              type,
-                                              passive,
-                                              durable,
-                                              autoDelete,
-                                              internal,
-                                              nowait,
-                                              FieldTable.convertToDecodedFieldTable(arguments));
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                dispatcher.receiveExchangeDeclare(exchange, type, passive, durable, autoDelete, internal, nowait,
+                        FieldTable.convertToDecodedFieldTable(arguments));
+            }
         }
-        if (arguments != null)
+        finally
         {
-            arguments.dispose();
+            if (arguments != null)
+            {
+                arguments.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java
index 1aa1ab2ace..6e338b00b7 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java
@@ -25,6 +25,8 @@ import java.io.IOException;
 
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
 import org.apache.qpid.server.transport.ByteBufferSender;
 
@@ -87,14 +89,30 @@ public class HeartbeatBody implements AMQBody
     }
 
     public static void process(final int channel,
-                            final QpidByteBuffer in,
-                            final MethodProcessor processor,
-                            final long bodySize)
+                               final QpidByteBuffer in,
+                               final MethodProcessor<?> processor,
+                               final long bodySize)
+            throws AMQFrameDecodingException
     {
-
-        if(bodySize > 0)
+        if (channel != 0)
+        {
+            // AMQP 0-8 and 0-9 define this as a framing error. AMQP 0-9-1's specific
+            // heartbeat-channel rule requires command-invalid
+            final int errorCode = ProtocolVersion.v0_91.equals(processor.getProtocolVersion())
+                    ? ErrorCodes.COMMAND_INVALID
+                    : ErrorCodes.FRAME_ERROR;
+            throw new AMQFrameDecodingException(errorCode, "Heartbeat frame must use channel 0, received channel " +
+                    channel, null);
+        }
+        if (bodySize > 0)
         {
-            in.position(in.position()+(int)bodySize);
+            if (ProtocolVersion.v0_91.equals(processor.getProtocolVersion()))
+            {
+                throw new AMQFrameDecodingException("AMQP 0-9-1 heartbeat frame must have an empty payload, " +
+                        "received " + bodySize + " bytes");
+            }
+            // AMQP 0-8 and 0-9 did not define an empty-payload requirement
+            in.position(in.position() + (int) bodySize);
         }
         processor.receiveHeartbeat();
     }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java
index e7b71a7ef5..b8bd74ae04 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java
@@ -21,6 +21,7 @@
 package org.apache.qpid.server.protocol.v0_8.transport;
 
 import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 
 public interface MethodProcessor<T extends ChannelMethodProcessor>
@@ -29,6 +30,13 @@ public interface MethodProcessor<T extends ChannelMethodProcessor>
 
     T getChannelMethodProcessor(int channelId);
 
+    default void receiveOversizedMessageHeader(final int channelId, final long bodySize)
+            throws AMQFrameDecodingException
+    {
+        throw new AMQFrameDecodingException("Content body size " + Long.toUnsignedString(bodySize) +
+                " exceeds the supported range", null);
+    }
+
     void receiveConnectionClose(int replyCode, AMQShortString replyText, int classId, int methodId);
 
     void receiveConnectionCloseOk();
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java
index bafcf9cc61..b49abe0584 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -164,20 +165,35 @@ public class QueueBindBody extends AMQMethodBodyImpl implements EncodableAMQData
     public static void process(final QpidByteBuffer buffer,
                                final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
     {
+        process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static void process(final QpidByteBuffer buffer,
+                               final ServerChannelMethodProcessor dispatcher,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
 
         int ticket = buffer.getUnsignedShort();
         AMQShortString queue = AMQShortString.readAMQShortString(buffer);
         AMQShortString exchange = AMQShortString.readAMQShortString(buffer);
         AMQShortString bindingKey = AMQShortString.readAMQShortString(buffer);
         boolean nowait = (buffer.get() & 0x01) == 0x01;
-        FieldTable arguments = EncodingUtils.readFieldTable(buffer);
-        if(!dispatcher.ignoreAllButCloseOk())
+        final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+        try
         {
-            dispatcher.receiveQueueBind(queue, exchange, bindingKey, nowait, FieldTable.convertToDecodedFieldTable(arguments));
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                final FieldTable decoded = FieldTable.convertToDecodedFieldTable(arguments);
+                dispatcher.receiveQueueBind(queue, exchange, bindingKey, nowait, decoded);
+            }
         }
-        if (arguments != null)
+        finally
         {
-            arguments.dispose();
+            if (arguments != null)
+            {
+                arguments.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java
index 9df4634bf8..1d76469902 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -192,6 +193,14 @@ public class QueueDeclareBody extends AMQMethodBodyImpl implements EncodableAMQD
     public static void process(final QpidByteBuffer buffer,
                                final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
     {
+        process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static void process(final QpidByteBuffer buffer,
+                               final ServerChannelMethodProcessor dispatcher,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
 
         int ticket = buffer.getUnsignedShort();
         AMQShortString queue = AMQShortString.readAMQShortString(buffer);
@@ -202,14 +211,21 @@ public class QueueDeclareBody extends AMQMethodBodyImpl implements EncodableAMQD
         boolean exclusive = (bitfield & 0x04 ) == 0x04;
         boolean autoDelete = (bitfield & 0x08 ) == 0x08;
         boolean nowait = (bitfield & 0x010 ) == 0x010;
-        FieldTable arguments = EncodingUtils.readFieldTable(buffer);
-        if(!dispatcher.ignoreAllButCloseOk())
+        final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+        try
         {
-            dispatcher.receiveQueueDeclare(queue, passive, durable, exclusive, autoDelete, nowait, FieldTable.convertToDecodedFieldTable(arguments));
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                final FieldTable decoded = FieldTable.convertToDecodedFieldTable(arguments);
+                dispatcher.receiveQueueDeclare(queue, passive, durable, exclusive, autoDelete, nowait, decoded);
+            }
         }
-        if (arguments != null)
+        finally
         {
-            arguments.dispose();
+            if (arguments != null)
+            {
+                arguments.dispose();
+            }
         }
     }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java
index 0cba587ab4..59bf07626f 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
 import org.apache.qpid.server.QpidException;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
 import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -147,19 +148,34 @@ public class QueueUnbindBody extends AMQMethodBodyImpl implements EncodableAMQDa
     public static void process(final QpidByteBuffer buffer,
                                final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
     {
+        process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    public static void process(final QpidByteBuffer buffer,
+                               final ServerChannelMethodProcessor dispatcher,
+                               final int maxNestedObjects)
+            throws AMQFrameDecodingException
+    {
 
         int ticket = buffer.getUnsignedShort();
         AMQShortString queue = AMQShortString.readAMQShortString(buffer);
         AMQShortString exchange = AMQShortString.readAMQShortString(buffer);
         AMQShortString routingKey = AMQShortString.readAMQShortString(buffer);
-        FieldTable arguments = EncodingUtils.readFieldTable(buffer);
-        if(!dispatcher.ignoreAllButCloseOk())
+        final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+        try
         {
-            dispatcher.receiveQueueUnbind(queue, exchange, routingKey, FieldTable.convertToDecodedFieldTable(arguments));
+            if (!dispatcher.ignoreAllButCloseOk())
+            {
+                final FieldTable decoded = FieldTable.convertToDecodedFieldTable(arguments);
+                dispatcher.receiveQueueUnbind(queue, exchange, routingKey, decoded);
+            }
         }
-        if (arguments != null)
+        finally
         {
-            arguments.dispose();
+            if (arguments != null)
+            {
+                arguments.dispose();
+            }
         }
 
     }
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java
index ac081a27e7..96c712e782 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java
@@ -25,6 +25,16 @@ import org.apache.qpid.server.protocol.v0_8.FieldTable;
 
 public interface ServerChannelMethodProcessor extends ChannelMethodProcessor
 {
+    /**
+     * Rejects the current method frame if a content sequence is awaiting its remaining content frames
+     *
+     * @return true when the method frame was rejected and must not be dispatched, false otherwise
+     */
+    default boolean rejectMethodFrameIfContentIncomplete()
+    {
+        return false;
+    }
+
     void receiveAccessRequest(AMQShortString realm,
                               boolean exclusive,
                               boolean passive,
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
index 282ce12a4c..57cbf43ba2 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
@@ -20,6 +20,7 @@
  */
 package org.apache.qpid.server.protocol.v0_8;
 
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -32,6 +33,7 @@ import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -46,6 +48,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.configuration.updater.TaskExecutor;
 import org.apache.qpid.server.connection.SessionPrincipal;
 import org.apache.qpid.server.logging.EventLogger;
@@ -119,6 +122,7 @@ class AMQChannelTest extends UnitTestBase
         when(_amqConnection.getSubject()).thenReturn(authenticatedSubject);
         when(_amqConnection.getAuthorizedPrincipal()).thenReturn(authenticatedPrincipal);
         when(_amqConnection.getAddressSpace()).thenReturn(_virtualHost);
+        when(_amqConnection.getProtocolVersion()).thenReturn(ProtocolVersion.v0_9);
         when(_amqConnection.getProtocolOutputConverter()).thenReturn(protocolOutputConverter);
         when(_amqConnection.getBroker()).thenReturn(broker);
         when(_amqConnection.getMethodRegistry()).thenReturn(new MethodRegistry(ProtocolVersion.v0_9));
@@ -127,6 +131,8 @@ class AMQChannelTest extends UnitTestBase
         when(_amqConnection.getContextValue(Integer.class, Session.PRODUCER_AUTH_CACHE_SIZE)).thenReturn(Session.PRODUCER_AUTH_CACHE_SIZE_DEFAULT);
         when(_amqConnection.getContextValue(Long.class, Connection.MAX_UNCOMMITTED_IN_MEMORY_SIZE)).thenReturn(Connection.DEFAULT_MAX_UNCOMMITTED_IN_MEMORY_SIZE);
         when(_amqConnection.getContextValue(Boolean.class, AMQPConnection_0_8.FORCE_MESSAGE_VALIDATION)).thenReturn(true);
+        when(_amqConnection.getContextValue(Integer.class, AMQPConnection_0_8.CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE))
+                .thenReturn(AMQPConnection_0_8.DEFAULT_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE);
         when(_amqConnection.getTaskExecutor()).thenReturn(taskExecutor);
         when(_amqConnection.getChildExecutor()).thenReturn(taskExecutor);
         when(_amqConnection.getModel()).thenReturn(BrokerModel.getInstance());
@@ -209,6 +215,192 @@ class AMQChannelTest extends UnitTestBase
 
     }
 
+    @Test
+    void unsignedOversizedMessageClosesChannel()
+    {
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+        final AMQChannel channel = new AMQChannel(_amqConnection, 1, _messageStore);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, AMQShortString.EMPTY_STRING, false, false);
+
+        channel.receiveOversizedMessageHeader(Long.MIN_VALUE);
+
+        verify(_amqConnection).closeChannelAndWriteFrame(channel, ErrorCodes.MESSAGE_TOO_LARGE,
+                "Content body size 9223372036854775808 exceeds the supported range");
+    }
+
+    @Test
+    void unsignedOversizedMessageWithoutPublishClosesConnection()
+    {
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+
+        channel.receiveOversizedMessageHeader(Long.MIN_VALUE);
+
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.COMMAND_INVALID,
+                "Attempt to send a content header without first sending a publish frame", channelId);
+    }
+
+    @Test
+    void unsignedOversizedDuplicateHeaderUsesContentSequenceError()
+    {
+        when(_amqConnection.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+        when(_amqConnection.getMaxMessageSize()).thenReturn(1L);
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+        final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+        when(properties.checkValid()).thenReturn(true);
+
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+        channel.receiveMessageHeader(properties, 1L);
+
+        channel.receiveOversizedMessageHeader(Long.MIN_VALUE);
+
+        verify(properties).dispose();
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.UNEXPECTED_FRAME,
+                "Attempt to send a duplicate content header", channelId);
+    }
+
+    @Test
+    void contentBodyFrameLimitDisposesIncompleteMessageAndClosesConnection()
+    {
+        final int maximumContentBodyFrames = 2;
+        when(_amqConnection.getContextValue(Integer.class,
+                AMQPConnection_0_8.CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE))
+                .thenReturn(maximumContentBodyFrames);
+        when(_amqConnection.getMaxMessageSize()).thenReturn(3L);
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+        final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+        when(properties.checkValid()).thenReturn(true);
+        final QpidByteBuffer content = mock(QpidByteBuffer.class);
+        final QpidByteBuffer firstRetainedContent = mock(QpidByteBuffer.class);
+        final QpidByteBuffer secondRetainedContent = mock(QpidByteBuffer.class);
+        when(content.remaining()).thenReturn(1);
+        when(content.duplicate()).thenReturn(firstRetainedContent, secondRetainedContent);
+
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+        channel.receiveMessageHeader(properties, 3L);
+
+        channel.receiveMessageContent(content);
+        channel.receiveMessageContent(content);
+        channel.receiveMessageContent(content);
+
+        verify(properties).dispose();
+        verify(firstRetainedContent).dispose();
+        verify(secondRetainedContent).dispose();
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.RESOURCE_ERROR,
+                "Message exceeds maximum number of content body frames (" + maximumContentBodyFrames + ")", channelId);
+        channel.dispose();
+    }
+
+    @Test
+    void excessiveContentBodyDisposesIncompleteMessageAndClosesConnection()
+    {
+        when(_amqConnection.getMaxMessageSize()).thenReturn(2L);
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+        final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+        when(properties.checkValid()).thenReturn(true);
+        final QpidByteBuffer content = mock(QpidByteBuffer.class);
+        final QpidByteBuffer retainedContent = mock(QpidByteBuffer.class);
+        when(content.remaining()).thenReturn(1);
+        when(content.duplicate()).thenReturn(retainedContent);
+        final QpidByteBuffer excessiveContent = mock(QpidByteBuffer.class);
+        when(excessiveContent.remaining()).thenReturn(2);
+
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+        channel.receiveMessageHeader(properties, 2L);
+        channel.receiveMessageContent(content);
+
+        channel.receiveMessageContent(excessiveContent);
+
+        verify(properties).dispose();
+        verify(retainedContent).dispose();
+        verify(excessiveContent, never()).duplicate();
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.FRAME_ERROR,
+                "More message data received than content header defined", channelId);
+        channel.dispose();
+    }
+
+    @Test
+    void contentBodyBeforeHeaderClosesConnection()
+    {
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+
+        try (final QpidByteBuffer emptyContent = QpidByteBuffer.wrap(new byte[0]))
+        {
+            channel.receiveMessageContent(emptyContent);
+        }
+
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.FRAME_ERROR,
+                "Attempt to send a content body before sending a content header", channelId);
+        channel.dispose();
+    }
+
+    @Test
+    void methodFrameRejectionDisposesIncompleteMessageAndClosesConnection()
+    {
+        when(_amqConnection.getMaxMessageSize()).thenReturn(2L);
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+        final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+        when(properties.checkValid()).thenReturn(true);
+        final QpidByteBuffer content = mock(QpidByteBuffer.class);
+        final QpidByteBuffer retainedContent = mock(QpidByteBuffer.class);
+        when(content.remaining()).thenReturn(1);
+        when(content.duplicate()).thenReturn(retainedContent);
+
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+        assertFalse(channel.rejectMethodFrameIfContentIncomplete());
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+        channel.receiveMessageHeader(properties, 2L);
+        channel.receiveMessageContent(content);
+
+        assertTrue(channel.rejectMethodFrameIfContentIncomplete());
+
+        verify(properties).dispose();
+        verify(retainedContent).dispose();
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.FRAME_ERROR,
+                "Method frame received before completing the previous message", channelId);
+        channel.dispose();
+    }
+
+    @Test
+    void duplicateContentHeaderDisposesBothHeadersAndClosesConnection()
+    {
+        when(_amqConnection.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+        when(_amqConnection.getMaxMessageSize()).thenReturn(1L);
+        when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+        final BasicContentHeaderProperties firstProperties = mock(BasicContentHeaderProperties.class);
+        when(firstProperties.checkValid()).thenReturn(true);
+        final BasicContentHeaderProperties secondProperties = mock(BasicContentHeaderProperties.class);
+
+        final int channelId = 1;
+        final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+        channel.receiveMessageHeader(firstProperties, 1L);
+
+        channel.receiveMessageHeader(secondProperties, 1L);
+
+        verify(firstProperties).dispose();
+        verify(secondProperties).dispose();
+        verify(_amqConnection).sendConnectionClose(ErrorCodes.UNEXPECTED_FRAME,
+                "Attempt to send a duplicate content header", channelId);
+        channel.dispose();
+    }
+
     @Test
     void publishContentHeaderWhenMessageAuthorizationFails()
     {
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
index a7912a3b72..9f69eaf5b3 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
@@ -20,28 +20,63 @@
  */
 package org.apache.qpid.server.protocol.v0_8;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
+import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
 
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLSession;
 import javax.security.auth.Subject;
 
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
 
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.configuration.updater.TaskExecutorImpl;
 import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.message.MessageDestination;
 import org.apache.qpid.server.model.AuthenticationProvider;
 import org.apache.qpid.server.model.Broker;
 import org.apache.qpid.server.model.BrokerModel;
@@ -56,8 +91,21 @@ import org.apache.qpid.server.model.VirtualHost;
 import org.apache.qpid.server.model.VirtualHostNode;
 import org.apache.qpid.server.model.port.AmqpPort;
 import org.apache.qpid.server.properties.ConnectionStartProperties;
+import org.apache.qpid.server.protocol.ErrorCodes;
 import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQFrame;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.FrameCreatingMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.HeartbeatBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ProtocolInitiation;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
 import org.apache.qpid.server.security.SubjectCreator;
 import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
 import org.apache.qpid.server.security.auth.AuthenticationResult;
@@ -65,10 +113,17 @@ import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
 import org.apache.qpid.server.security.auth.UsernamePrincipal;
 import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
 import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import org.apache.qpid.server.store.StoreException;
 import org.apache.qpid.server.transport.AMQPConnection;
 import org.apache.qpid.server.transport.AggregateTicker;
 import org.apache.qpid.server.transport.ByteBufferSender;
+import org.apache.qpid.server.transport.NonBlockingConnection;
+import org.apache.qpid.server.transport.NonBlockingConnectionPlainDelegate;
+import org.apache.qpid.server.transport.NonBlockingConnectionTLSDelegate;
 import org.apache.qpid.server.transport.ServerNetworkConnection;
+import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
 import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
 import org.apache.qpid.server.virtualhost.VirtualHostPrincipal;
 import org.apache.qpid.test.utils.UnitTestBase;
@@ -80,6 +135,7 @@ class AMQPConnection_0_8Test extends UnitTestBase
     private static final byte[] SASL_RESPONSE = "response".getBytes();
     private static final AMQShortString LOCALE = AMQShortString.createAMQShortString("en_US");
     private static final AMQShortString SASL_MECH = AMQShortString.createAMQShortString("MECH");
+    private static final byte FRAME_END = (byte) 0xCE;
 
     private TaskExecutorImpl _taskExecutor;
     private Broker _broker;
@@ -89,6 +145,9 @@ class AMQPConnection_0_8Test extends UnitTestBase
     private Transport _transport;
     private Protocol _protocol;
     private AggregateTicker _ticker;
+    private AuthenticationProvider _authenticationProvider;
+    private SubjectCreator _subjectCreator;
+    private ByteBufferSender _sender;
 
     @BeforeEach
     void setUp() throws Exception
@@ -135,31 +194,32 @@ class AMQPConnection_0_8Test extends UnitTestBase
         when(_virtualHost.authoriseCreateConnection(any(AMQPConnection.class))).thenReturn(true);
         when(_virtualHost.getEventLogger()).thenReturn(value);
 
-        final SubjectCreator subjectCreator = mock(SubjectCreator.class);
+        _subjectCreator = mock(SubjectCreator.class);
 
-        final SaslNegotiator saslNegotiator = mock(SaslNegotiator.class);
-        when(subjectCreator.createSaslNegotiator(eq(SASL_MECH.toString()), any(SaslSettings.class))).thenReturn(saslNegotiator);
-        when(subjectCreator.authenticate(saslNegotiator, SASL_RESPONSE)).thenReturn(new SubjectAuthenticationResult(
+        final SaslNegotiator _saslNegotiator = mock(SaslNegotiator.class);
+        when(_subjectCreator.createSaslNegotiator(eq(SASL_MECH.toString()), any(SaslSettings.class)))
+                .thenReturn(_saslNegotiator);
+        when(_subjectCreator.authenticate(_saslNegotiator, SASL_RESPONSE)).thenReturn(new SubjectAuthenticationResult(
                 new AuthenticationResult(new AuthenticatedPrincipal(new UsernamePrincipal("username", null))), new Subject()));
 
-        final AuthenticationProvider authenticationProvider = mock(AuthenticationProvider.class);
-        when(authenticationProvider.getAvailableMechanisms(anyBoolean())).thenReturn(List.of(SASL_MECH.toString()));
+        _authenticationProvider = mock(AuthenticationProvider.class);
+        when(_authenticationProvider.getAvailableMechanisms(anyBoolean())).thenReturn(List.of(SASL_MECH.toString()));
 
         _port = mock(AmqpPort.class);
         when(_port.getParent()).thenReturn(_broker);
         when(_port.getCategoryClass()).thenReturn(Port.class);
         when(_port.getChildExecutor()).thenReturn(_taskExecutor);
         when(_port.getModel()).thenReturn(model);
-        when(_port.getAuthenticationProvider()).thenReturn(authenticationProvider);
+        when(_port.getAuthenticationProvider()).thenReturn(_authenticationProvider);
         when(_port.getAddressSpace(VIRTUAL_HOST_NAME)).thenReturn(_virtualHost);
         when(_port.getContextValue(Long.class, Port.CONNECTION_MAXIMUM_AUTHENTICATION_DELAY)).thenReturn(2500L);
         when(_port.getContextValue(Integer.class, Connection.MAX_MESSAGE_SIZE)).thenReturn(Connection.DEFAULT_MAX_MESSAGE_SIZE);
-        when(_port.getSubjectCreator(eq(false), anyString())).thenReturn(subjectCreator);
+        when(_port.getSubjectCreator(eq(false), anyString())).thenReturn(_subjectCreator);
 
-        final ByteBufferSender sender = mock(ByteBufferSender.class);
+        _sender = mock(ByteBufferSender.class);
 
         _network = mock(ServerNetworkConnection.class);
-        when(_network.getSender()).thenReturn(sender);
+        when(_network.getSender()).thenReturn(_sender);
         when(_network.getLocalAddress()).thenReturn(new InetSocketAddress("localhost", 12345));
         when(_network.getSelectedHost()).thenReturn("localhost");
 
@@ -174,12 +234,64 @@ class AMQPConnection_0_8Test extends UnitTestBase
         _taskExecutor.stopImmediately();
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"UNKNOWN", "DISABLED", "SECURE_ONLY"})
+    void testUnadvertisedSaslMechanismRejected(final String mechanismName)
+    {
+        final AMQPConnection_0_8Impl connection = createConnectionAwaitingStartOk();
+        clearInvocations(_network, _sender);
+
+        connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()),
+                AMQShortString.createAMQShortString(mechanismName), SASL_RESPONSE, LOCALE);
+
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+        verify(_subjectCreator, never()).createSaslNegotiator(eq(mechanismName), any(SaslSettings.class));
+    }
+
+    @ParameterizedTest
+    @NullAndEmptySource
+    void testMissingSaslMechanismClosesNetworkWithoutResponse(final String mechanismName)
+    {
+        final AMQPConnection_0_8Impl connection = createConnectionAwaitingStartOk();
+        clearInvocations(_network, _sender);
+        final AMQShortString mechanism = mechanismName == null
+                ? null
+                : AMQShortString.createAMQShortString(mechanismName);
+
+        connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()), mechanism,
+                SASL_RESPONSE, LOCALE);
+
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+        verify(_subjectCreator, never()).createSaslNegotiator(anyString(), any(SaslSettings.class));
+    }
+
+    @Test
+    void testMechanismAddedAfterAdvertisementRejected()
+    {
+        final String mechanismName = "NEWLY_ENABLED";
+        final List<String> availableMechanisms = new ArrayList<>(List.of(SASL_MECH.toString()));
+        when(_authenticationProvider.getAvailableMechanisms(false)).thenReturn(availableMechanisms);
+
+        final AMQPConnection_0_8Impl connection = createConnectionAwaitingStartOk();
+        availableMechanisms.add(mechanismName);
+        clearInvocations(_network, _sender);
+
+        connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()),
+                AMQShortString.createAMQShortString(mechanismName), SASL_RESPONSE, LOCALE);
+
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+        verify(_subjectCreator, never()).createSaslNegotiator(eq(mechanismName), any(SaslSettings.class));
+    }
+
     @Test
     void closeOnNoRoute()
     {
         {
-            final AMQPConnection_0_8Impl
-                    conn = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+            final AMQPConnection_0_8Impl conn =
+                    new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
             conn.create();
             conn.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
 
@@ -206,8 +318,8 @@ class AMQPConnection_0_8Test extends UnitTestBase
     @Test
     void connectionEnforcesMaxSessions()
     {
-        final AMQPConnection_0_8Impl
-                conn = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+        final AMQPConnection_0_8Impl conn = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                _protocol, 0, _ticker);
         conn.create();
 
         conn.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
@@ -235,11 +347,560 @@ class AMQPConnection_0_8Test extends UnitTestBase
         assertTrue(conn.isClosing(), "Connection should be closed after opening " + (maxChannels + 1) + " channels");
     }
 
+    @Test
+    void nestingViolationBeforeOpenClosesNetworkWithoutResponse()
+    {
+        final RecordingAMQPConnection connection = new RecordingAMQPConnection(_broker, _network, _port, _transport,
+                _protocol, _ticker);
+        connection.create();
+        connection.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer buffer = createConnectionStartOkFrame(
+                buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 1),
+                SASL_MECH.toString(), SASL_RESPONSE, LOCALE.toString()))
+        {
+            assertDoesNotThrow(() -> connection.received(buffer));
+        }
+
+        assertEquals(0, connection.getCloseCount());
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void nestingViolationAfterOpenProducesResourceError()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+                Protocol.AMQP_0_8);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createQueueDeclareFrame(
+                buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 1)))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(ErrorCodes.RESOURCE_ERROR, connection.getCloseErrorCode());
+        assertEquals(0, connection.getCloseChannelId());
+        assertEquals(QueueDeclareBody.CLASS_ID, connection.getCloseClassId());
+        assertEquals(QueueDeclareBody.METHOD_ID, connection.getCloseMethodId());
+        assertEquals(1, connection.getCloseCount());
+        verify(_network, never()).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void codedDecodingErrorAfterOpenProducesConnectionClose()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+                Protocol.AMQP_0_8);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createMethodFrame(1, ConnectionTuneOkBody.CLASS_ID,
+                ConnectionTuneOkBody.METHOD_ID))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(ErrorCodes.COMMAND_INVALID, connection.getCloseErrorCode());
+        assertEquals(ConnectionTuneOkBody.CLASS_ID, connection.getCloseClassId());
+        assertEquals(ConnectionTuneOkBody.METHOD_ID, connection.getCloseMethodId());
+        assertEquals(1, connection.getCloseCount());
+        verify(_network, never()).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void truncatedMethodAfterOpenProducesFrameErrorWithMethodIdentifiers()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+                Protocol.AMQP_0_9_1);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createMethodFrame(1, BasicQosBody.CLASS_ID, BasicQosBody.METHOD_ID))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, connection.getCloseErrorCode());
+        assertEquals(BasicQosBody.CLASS_ID, connection.getCloseClassId());
+        assertEquals(BasicQosBody.METHOD_ID, connection.getCloseMethodId());
+        assertEquals(1, connection.getCloseCount());
+        verify(_network, never()).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @ParameterizedTest
+    @CsvSource({"false, false", "false, true", "true, false", "true, true"})
+    void testDecodingFailureWithoutProgressClosesTransport(final boolean direct, final boolean initialProgress)
+            throws Exception
+    {
+        final AtomicInteger decodeCount = new AtomicInteger();
+        try (final MockedConstruction<BrokerDecoder> construction = mockConstruction(BrokerDecoder.class,
+                (decoder, context) -> doAnswer(invocation ->
+                {
+                    final QpidByteBuffer input = invocation.getArgument(0);
+                    if (decodeCount.getAndIncrement() == 0 && initialProgress)
+                    {
+                        input.position(input.position() + Integer.BYTES);
+                    }
+                    throw new AMQFrameDecodingException("Frame header validation failed");
+                }).when(decoder).decodeBuffer(any(QpidByteBuffer.class))))
+        {
+            final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                    _protocol, 0, _ticker);
+            openConnection(connection, ProtocolVersion.v0_8);
+            clearInvocations(_network, _sender);
+
+            try (final QpidByteBuffer input = QpidByteBuffer.allocate(direct, 16))
+            {
+                connection.received(input);
+
+                assertFalse(input.hasRemaining(), "Failed input must be discarded before shutdown");
+                verify(_network).close();
+
+                input.clear();
+                connection.received(input);
+
+                assertFalse(input.hasRemaining(), "Input arriving during transport shutdown must be discarded");
+                verify(construction.constructed().get(0), times(initialProgress ? 2 : 1))
+                        .decodeBuffer(any(QpidByteBuffer.class));
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("terminalDecodingFailures")
+    void testTerminalDecodingExceptionDiscardsPendingInput(final Exception failure, final boolean direct)
+            throws Exception
+    {
+        try (final MockedConstruction<BrokerDecoder> construction = mockConstruction(BrokerDecoder.class,
+                (decoder, context) -> doThrow(failure).when(decoder).decodeBuffer(any(QpidByteBuffer.class))))
+        {
+            final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                    _protocol, 0, _ticker);
+            openConnection(connection, ProtocolVersion.v0_8);
+            try (final QpidByteBuffer input = QpidByteBuffer.allocate(direct, 16))
+            {
+                final ConnectionScopedRuntimeException exception = assertThrows(ConnectionScopedRuntimeException.class,
+                        () -> connection.received(input));
+                assertSame(failure, exception.getCause());
+                assertFalse(input.hasRemaining());
+
+                input.clear();
+                connection.received(input);
+                assertFalse(input.hasRemaining());
+                verify(construction.constructed().get(0)).decodeBuffer(any(QpidByteBuffer.class));
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testCloseHandshakeDiscardsPendingInput(final boolean direct)
+    {
+        final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                _protocol, 0, _ticker);
+        openConnection(connection, ProtocolVersion.v0_8);
+        connection.sendConnectionClose(ErrorCodes.FRAME_ERROR, "Closing connection", 0);
+        clearInvocations(_network);
+
+        try (final QpidByteBuffer reply = createFrame(connection.getMethodRegistry().createConnectionCloseOkBody()
+                .generateFrame(0));
+             final QpidByteBuffer input = QpidByteBuffer.allocate(direct, reply.remaining() + 3))
+        {
+            input.put(reply);
+            input.put(new byte[3]);
+            input.flip();
+            connection.received(input);
+            assertFalse(input.hasRemaining());
+        }
+        verify(_network).close();
+    }
+
+    @ParameterizedTest
+    @MethodSource("rejectedFrameCloseReadVariants")
+    void testRejectedFrameCleanupAllowsOrderlyClose(final ProtocolVersion protocolVersion,
+                                                   final Protocol protocol,
+                                                   final int split,
+                                                   final boolean peerCloses,
+                                                   final boolean oversized) throws Exception
+    {
+        final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                protocol, 0, _ticker);
+        openConnection(connection, protocolVersion);
+        connection.setMaxFrameSize(AMQDecoder.FRAME_MIN_SIZE);
+        clearInvocations(_network, _sender);
+        final ByteBuffer output = ByteBuffer.allocate(1024);
+        doAnswer(invocation ->
+        {
+            final QpidByteBuffer buffer = invocation.getArgument(0);
+            buffer.copyTo(output);
+            return null;
+        }).when(_sender).send(any(QpidByteBuffer.class));
+
+        final AMQFrame response = peerCloses
+                ? new ConnectionCloseBody(protocolVersion, ErrorCodes.REPLY_SUCCESS, AMQShortString.EMPTY_STRING,
+                        0, 0).generateFrame(0)
+                : connection.getMethodRegistry().createConnectionCloseOkBody().generateFrame(0);
+        try (final QpidByteBuffer rejected = oversized ?
+                     createFrame(ContentBody.TYPE, 1, new byte[AMQDecoder.FRAME_MIN_SIZE + 1], FRAME_END) :
+                     createMethodFrame(1, BasicQosBody.CLASS_ID, BasicQosBody.METHOD_ID);
+             final QpidByteBuffer reply = createFrame(response);
+             final QpidByteBuffer buffer = QpidByteBuffer.allocate(rejected.remaining() + reply.remaining()))
+        {
+            final int rejectedFrameSize = rejected.remaining();
+            final byte[] input = new byte[buffer.capacity()];
+            rejected.get(input, 0, rejectedFrameSize);
+            reply.get(input, rejectedFrameSize, reply.remaining());
+            final int firstReadLength = split < 0 ? input.length : split;
+            buffer.put(input, 0, firstReadLength);
+            buffer.flip();
+            connection.received(buffer);
+
+            final int rejectionThreshold = oversized ? AMQDecoder.FRAME_HEADER_SIZE : rejectedFrameSize;
+            if (firstReadLength < rejectionThreshold)
+            {
+                assertEquals(0, buffer.position());
+                assertFalse(connection.isClosing());
+            }
+            else
+            {
+                assertTrue(connection.isClosing());
+            }
+
+            if (oversized && firstReadLength >= rejectionThreshold && firstReadLength <= rejectedFrameSize)
+            {
+                assertFalse(buffer.hasRemaining(), "Rejected payload must not be retained for the next read");
+            }
+
+            if (firstReadLength < input.length)
+            {
+                verify(_network, never()).close();
+                buffer.compact();
+                buffer.put(input, firstReadLength, input.length - firstReadLength);
+                buffer.flip();
+                connection.received(buffer);
+            }
+
+            assertFalse(buffer.hasRemaining());
+            assertTrue(connection.isClosing());
+            verify(_network).close();
+        }
+
+        output.flip();
+        final FrameCreatingMethodProcessor methodProcessor = new FrameCreatingMethodProcessor(protocolVersion);
+        new ClientDecoder(methodProcessor).decodeBuffer(output);
+        final List<AMQDataBlock> frames = methodProcessor.getProcessedMethods();
+        assertEquals(peerCloses ? 2 : 1, frames.size());
+        final ConnectionCloseBody close = assertInstanceOf(ConnectionCloseBody.class,
+                ((AMQFrame) frames.get(0)).getBodyFrame());
+        assertEquals(ErrorCodes.FRAME_ERROR, close.getReplyCode());
+        assertEquals(oversized ? 0 : BasicQosBody.CLASS_ID, close.getClassId());
+        assertEquals(oversized ? 0 : BasicQosBody.METHOD_ID, close.getMethodId());
+        if (peerCloses)
+        {
+            assertInstanceOf(ConnectionCloseOkBody.class, ((AMQFrame) frames.get(1)).getBodyFrame());
+        }
+    }
+
+    @Test
+    void testIncompleteDiscardRetainsCloseTimeout()
+    {
+        final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                _protocol, 0, _ticker);
+        openConnection(connection, ProtocolVersion.v0_8);
+        connection.setMaxFrameSize(AMQDecoder.FRAME_MIN_SIZE);
+        clearInvocations(_network);
+
+        try (final QpidByteBuffer frame = createFrame(ContentBody.TYPE, 1, new byte[AMQDecoder.FRAME_MIN_SIZE + 1],
+                FRAME_END))
+        {
+            frame.limit(AMQDecoder.FRAME_HEADER_SIZE);
+            connection.received(frame);
+            assertTrue(connection.isClosing());
+            assertFalse(frame.hasRemaining());
+            verify(_network, never()).close();
+
+            final long closeTimeout = connection.getContextValue(Long.class, Connection.CLOSE_RESPONSE_TIMEOUT);
+            _ticker.tick(System.currentTimeMillis() + closeTimeout + 1);
+            verify(_network).close();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testRejectedFrameKeepsTransportBuffersBounded(final boolean tls) throws Exception
+    {
+        final int networkBufferSize = 128;
+        QpidByteBuffer.deinitialisePool();
+        QpidByteBuffer.initialisePool(networkBufferSize, 16, 1.0);
+        try (final MockedStatic<SSLUtil> sslUtil = mockStatic(SSLUtil.class))
+        {
+            sslUtil.when(() -> SSLUtil.isSufficientToDetermineClientSNIHost(any(QpidByteBuffer.class)))
+                    .thenReturn(true);
+            final AMQPConnection_0_8Impl connection =
+                    new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+            openConnection(connection, ProtocolVersion.v0_8);
+            connection.setMaxFrameSize(AMQDecoder.FRAME_MIN_SIZE);
+            clearInvocations(_network, _sender);
+            final NonBlockingConnection network = mock(NonBlockingConnection.class);
+            when(_port.getNetworkBufferSize()).thenReturn(networkBufferSize);
+            doAnswer(invocation ->
+            {
+                final QpidByteBuffer input = invocation.getArgument(0);
+                assertTrue(input.capacity() <= networkBufferSize, "Application buffer grew during shutdown");
+                connection.received(input);
+                assertFalse(input.hasRemaining(), "Shutdown retained rejected input");
+                return null;
+            }).when(network).processAmqpData(any(QpidByteBuffer.class));
+
+            final Supplier<QpidByteBuffer> inputBuffer;
+            final Callable<Boolean> processInput;
+            final Runnable close;
+            if (tls)
+            {
+                final NonBlockingConnectionTLSDelegate delegate = createTlsDelegate(network, networkBufferSize);
+                inputBuffer = delegate::getNetInputBuffer;
+                processInput = delegate::processData;
+                close = () ->
+                {
+                    delegate.shutdownInput();
+                    delegate.shutdownOutput();
+                };
+            }
+            else
+            {
+                final NonBlockingConnectionPlainDelegate delegate =
+                        new NonBlockingConnectionPlainDelegate(network, _port);
+                inputBuffer = delegate::getNetInputBuffer;
+                processInput = delegate::processData;
+                close = () ->
+                {
+                    delegate.shutdownInput();
+                    delegate.shutdownOutput();
+                };
+            }
+            try
+            {
+                try (final QpidByteBuffer rejected = createFrame(ContentBody.TYPE, 1,
+                            new byte[AMQDecoder.FRAME_MIN_SIZE + 1], FRAME_END);
+                     final QpidByteBuffer reply = createFrame(connection.getMethodRegistry()
+                            .createConnectionCloseOkBody().generateFrame(0)))
+                {
+                    final byte[] input = new byte[rejected.remaining() + reply.remaining()];
+                    final int rejectedSize = rejected.remaining();
+                    rejected.get(input, 0, rejectedSize);
+                    reply.get(input, rejectedSize, reply.remaining());
+                    for (int offset = 0; offset < input.length;)
+                    {
+                        final int length = Math.min(networkBufferSize, input.length - offset);
+                        inputBuffer.get().put(input, offset, length);
+                        processInput.call();
+                        assertTrue(inputBuffer.get().capacity() <= networkBufferSize);
+                        offset += length;
+                    }
+                }
+
+                verify(_network).close();
+                inputBuffer.get().put(new byte[inputBuffer.get().remaining()]);
+                processInput.call();
+                assertEquals(networkBufferSize, inputBuffer.get().capacity());
+            }
+            finally
+            {
+                close.run();
+            }
+            assertEquals(0, QpidByteBuffer.getAllocatedDirectMemorySize(), "Shutdown retained pooled direct memory");
+        }
+        finally
+        {
+            QpidByteBuffer.deinitialisePool();
+        }
+    }
+
+    private NonBlockingConnectionTLSDelegate createTlsDelegate(final NonBlockingConnection network,
+                                                               final int networkBufferSize) throws Exception
+    {
+        final SSLContext sslContext = mock(SSLContext.class);
+        final SSLEngine sslEngine = mock(SSLEngine.class);
+        final SSLSession session = mock(SSLSession.class);
+        when(_port.getSSLContext()).thenReturn(sslContext);
+        when(_port.getContextValue(Boolean.class, AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING)).thenReturn(false);
+        when(_port.getContextValue(Integer.class, AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_WARN_THRESHOLD))
+                .thenReturn(0);
+        when(_port.getContextValue(Integer.class, AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD))
+                .thenReturn(0);
+        when(sslContext.createSSLEngine()).thenReturn(sslEngine);
+        when(sslEngine.getSession()).thenReturn(session);
+        when(session.getPacketBufferSize()).thenReturn(networkBufferSize);
+        when(session.getApplicationBufferSize()).thenReturn(networkBufferSize);
+
+        when(sslEngine.unwrap(any(ByteBuffer.class), any(ByteBuffer.class))).thenAnswer(invocation ->
+        {
+            final ByteBuffer source = invocation.getArgument(0);
+            final ByteBuffer target = invocation.getArgument(1);
+            final int length = source.remaining();
+            target.put(source);
+            return new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING,
+                    length, length);
+        });
+        return new NonBlockingConnectionTLSDelegate(network, _port);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testFatalDecodingFailureDiscardsPendingInput(final boolean direct)
+    {
+        final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                _protocol, 0, _ticker);
+        openConnection(connection, ProtocolVersion.v0_8);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createFrame(HeartbeatBody.TYPE, 0, new byte[0], (byte) 0);
+             final QpidByteBuffer input = QpidByteBuffer.allocate(direct, frame.remaining() + 3))
+        {
+            input.put(frame);
+            input.put(new byte[3]);
+            input.flip();
+            connection.received(input);
+
+            assertFalse(input.hasRemaining());
+            input.clear();
+            connection.received(input);
+            assertFalse(input.hasRemaining());
+        }
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void malformedHeartbeatAfterOpenProducesConnectionClose()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+                Protocol.AMQP_0_9_1);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createFrame(HeartbeatBody.TYPE, 0, new byte[] { 1 }, FRAME_END))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, connection.getCloseErrorCode());
+        assertEquals(1, connection.getCloseCount());
+        verify(_network, never()).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void unsignedContentBodySizeAfterOpenProducesChannelClose()
+    {
+        when(_virtualHost.getDefaultDestination()).thenReturn(mock(MessageDestination.class));
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+                Protocol.AMQP_0_9_1);
+        connection.receiveChannelOpen(1);
+        final AMQChannel channel = connection.getChannel(1);
+        channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, AMQShortString.EMPTY_STRING, false, false);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createContentHeaderFrame(1, Long.MIN_VALUE, new byte[] { (byte) 0xFF }))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(0, connection.getCloseCount());
+        assertEquals(1, connection.getChannelCloseCount());
+        assertSame(channel, connection.getClosedChannel());
+        assertEquals(ErrorCodes.MESSAGE_TOO_LARGE, connection.getChannelCloseErrorCode());
+        assertEquals("Content body size 9223372036854775808 exceeds the supported range",
+                connection.getChannelCloseMessage());
+        verify(_network, never()).close();
+    }
+
+    @Test
+    void unsignedContentBodySizeOnUnknownChannelProducesChannelError()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+                Protocol.AMQP_0_9_1);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createContentHeaderFrame(1, Long.MIN_VALUE, new byte[0]))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(ErrorCodes.CHANNEL_ERROR, connection.getCloseErrorCode());
+        assertEquals(1, connection.getCloseChannelId());
+        assertEquals(1, connection.getCloseCount());
+        assertEquals(0, connection.getChannelCloseCount());
+        verify(_network, never()).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void unsignedContentBodySizeBeforeOpenClosesNetworkWithoutResponse()
+    {
+        final RecordingAMQPConnection connection = new RecordingAMQPConnection(_broker, _network, _port, _transport,
+                _protocol, _ticker);
+        connection.create();
+        connection.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createContentHeaderFrame(1, Long.MIN_VALUE, new byte[0]))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(0, connection.getCloseCount());
+        assertEquals(0, connection.getChannelCloseCount());
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void invalidFrameEndAfterOpenClosesNetworkWithoutResponse()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+                Protocol.AMQP_0_8);
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createFrame(HeartbeatBody.TYPE, 0, new byte[0], (byte) 0))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(0, connection.getCloseCount());
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+    }
+
+    @Test
+    void unsupportedFrameTypeAfterOpenClosesNetworkWithoutResponse()
+    {
+        final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+                Protocol.AMQP_0_8);
+        try (final QpidByteBuffer heartbeat = createFrame(HeartbeatBody.TYPE, 0, new byte[0], FRAME_END))
+        {
+            assertDoesNotThrow(() -> connection.received(heartbeat));
+        }
+        clearInvocations(_network, _sender);
+
+        try (final QpidByteBuffer frame = createFrame(Byte.MAX_VALUE, 0, new byte[0], FRAME_END))
+        {
+            assertDoesNotThrow(() -> connection.received(frame));
+        }
+
+        assertEquals(0, connection.getCloseCount());
+        verify(_network).close();
+        verifyNoInteractions(_sender);
+    }
+
     @Test
     void resetStatistics()
     {
-        final AMQPConnection_0_8Impl connection =
-                new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+        final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                _protocol, 0, _ticker);
         connection.create();
         connection.setAddressSpace(_virtualHost);
         connection.registerMessageReceived(100L);
@@ -265,4 +926,296 @@ class AMQPConnection_0_8Test extends UnitTestBase
         assertEquals(0L, statisticsAfterReset.get("transactedMessagesIn"));
         assertEquals(0L, statisticsAfterReset.get("transactedMessagesOut"));
     }
+
+    private static QpidByteBuffer createConnectionStartOkFrame(final byte[] fieldTable,
+                                                               final String mechanism,
+                                                               final byte[] response,
+                                                               final String locale)
+    {
+        final byte[] mechanismBytes = mechanism.getBytes(StandardCharsets.UTF_8);
+        final byte[] localeBytes = locale.getBytes(StandardCharsets.UTF_8);
+        final int bodySize = Integer.BYTES + Integer.BYTES + fieldTable.length + 1 + mechanismBytes.length +
+                Integer.BYTES + response.length + 1 + localeBytes.length;
+        final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+        frame.put((byte) 1);
+        frame.putUnsignedShort(0);
+        frame.putUnsignedInt(bodySize);
+        frame.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+        frame.putUnsignedInt(fieldTable.length);
+        frame.put(fieldTable);
+        frame.put((byte) mechanismBytes.length);
+        frame.put(mechanismBytes);
+        frame.putUnsignedInt(response.length);
+        frame.put(response);
+        frame.put((byte) localeBytes.length);
+        frame.put(localeBytes);
+        frame.put(FRAME_END);
+        frame.flip();
+        return frame;
+    }
+
+    private static QpidByteBuffer createQueueDeclareFrame(final byte[] arguments)
+    {
+        final int bodySize = Integer.BYTES + Short.BYTES + 2 + Integer.BYTES + arguments.length;
+        final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+        frame.put((byte) 1);
+        frame.putUnsignedShort(1);
+        frame.putUnsignedInt(bodySize);
+        frame.putInt((QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID);
+        frame.putUnsignedShort(0);
+        frame.put((byte) 0);
+        frame.put((byte) 0);
+        frame.putUnsignedInt(arguments.length);
+        frame.put(arguments);
+        frame.put(FRAME_END);
+        frame.flip();
+        return frame;
+    }
+
+    private static QpidByteBuffer createMethodFrame(final int channelId,
+                                                    final int classId,
+                                                    final int methodId)
+    {
+        final ByteBuffer body = ByteBuffer.allocate(Integer.BYTES);
+        body.putInt((classId << 16) | methodId);
+        return createFrame((byte) 1, channelId, body.array(), FRAME_END);
+    }
+
+    private static QpidByteBuffer createContentHeaderFrame(final int channelId,
+                                                           final long bodySize,
+                                                           final byte[] properties)
+    {
+        final ByteBuffer body = ByteBuffer.allocate(Short.BYTES * 3 + Long.BYTES + properties.length);
+        body.putShort((short) ContentHeaderBody.CLASS_ID);
+        body.putShort((short) 0);
+        body.putLong(bodySize);
+        body.putShort((short) 0);
+        body.put(properties);
+        return createFrame(ContentHeaderBody.TYPE, channelId, body.array(), FRAME_END);
+    }
+
+    private static QpidByteBuffer createFrame(final AMQFrame frame)
+    {
+        final QpidByteBuffer buffer = QpidByteBuffer.allocate((int) frame.getSize());
+        final ByteBufferSender sender = mock(ByteBufferSender.class);
+        doAnswer(invocation ->
+        {
+            final QpidByteBuffer payload = invocation.getArgument(0);
+            buffer.put(payload);
+            return null;
+        }).when(sender).send(any(QpidByteBuffer.class));
+        frame.writePayload(sender);
+        buffer.flip();
+        return buffer;
+    }
+
+    @ParameterizedTest
+    @MethodSource("scopedAndFatalFailures")
+    void testReceivePreservesScopedAndFatalFailures(final Throwable failure) throws Exception
+    {
+        try (final MockedConstruction<BrokerDecoder> construction = mockConstruction(BrokerDecoder.class,
+                (decoder, context) -> doThrow(failure).when(decoder).decodeBuffer(any(QpidByteBuffer.class))))
+        {
+            final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                    _protocol, 0, _ticker);
+            try (final QpidByteBuffer input = QpidByteBuffer.allocate(16))
+            {
+                assertSame(failure, assertThrows(failure.getClass(), () -> connection.onReceive(input)));
+                verify(construction.constructed().get(0)).decodeBuffer(input);
+            }
+        }
+    }
+
+    private static Stream<Arguments> terminalDecodingFailures()
+    {
+        return Stream.of(new IOException("Injected decoding failure"),
+                new ArithmeticException("Injected decoding failure"), new RuntimeException("Injected decoding failure"))
+                .flatMap(failure -> Stream.of(false, true).map(direct -> Arguments.of(failure, direct)));
+    }
+
+    private static Stream<Throwable> scopedAndFatalFailures()
+    {
+        return Stream.of(new ConnectionScopedRuntimeException("Injected connection failure"),
+                new ServerScopedRuntimeException("Injected server failure"),
+                new StoreException("Injected store failure"), new InternalError("Injected JVM failure"));
+    }
+
+    private static Stream<Arguments> rejectedFrameCloseReadVariants()
+    {
+        return Stream.of(Arguments.of(ProtocolVersion.v0_8, Protocol.AMQP_0_8),
+                Arguments.of(ProtocolVersion.v0_9, Protocol.AMQP_0_9),
+                Arguments.of(ProtocolVersion.v0_91, Protocol.AMQP_0_9_1))
+                .flatMap(arguments -> Stream.of(3, 7, 11, 12, 15, -1)
+                        .flatMap(split -> Stream.of(false, true)
+                                .flatMap(peerCloses -> Stream.of(false, true)
+                                        .map(oversized -> Arguments.of(arguments.get()[0], arguments.get()[1],
+                                                                      split, peerCloses, oversized)))));
+    }
+
+    private static QpidByteBuffer createFrame(final byte type,
+                                              final int channelId,
+                                              final byte[] body,
+                                              final byte frameEnd)
+    {
+        final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + body.length + 1);
+        frame.put(type);
+        frame.putUnsignedShort(channelId);
+        frame.putUnsignedInt(body.length);
+        frame.put(body);
+        frame.put(frameEnd);
+        frame.flip();
+        return frame;
+    }
+
+    private static byte[] buildNestedTable(final int depth)
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+        for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+        {
+            buffer.put((byte) 1);
+            buffer.put((byte) 'n');
+            buffer.put(AMQType.FIELD_TABLE.identifier());
+            buffer.putInt(7 * (remainingDepth - 1));
+        }
+        buffer.put((byte) 1);
+        buffer.put((byte) 'v');
+        buffer.put(AMQType.INT.identifier());
+        buffer.putInt(42);
+        return buffer.array();
+    }
+
+    private static final class RecordingAMQPConnection extends AMQPConnection_0_8Impl
+    {
+        private int _closeErrorCode;
+        private int _closeChannelId;
+        private int _closeClassId;
+        private int _closeMethodId;
+        private int _closeCount;
+        private AMQChannel _closedChannel;
+        private int _channelCloseErrorCode;
+        private String _channelCloseMessage;
+        private int _channelCloseCount;
+
+        private RecordingAMQPConnection(final Broker broker,
+                                        final ServerNetworkConnection network,
+                                        final AmqpPort port,
+                                        final Transport transport,
+                                        final Protocol protocol,
+                                        final AggregateTicker ticker)
+        {
+            super(broker, network, port, transport, protocol, 0, ticker);
+        }
+
+        @Override
+        public void sendConnectionClose(final int errorCode, final String message, final int channelId)
+        {
+            recordConnectionClose(errorCode, channelId, 0, 0);
+        }
+
+        @Override
+        protected void sendConnectionClose(final int errorCode,
+                                           final String message,
+                                           final int channelId,
+                                           final int classId,
+                                           final int methodId)
+        {
+            recordConnectionClose(errorCode, channelId, classId, methodId);
+        }
+
+        @Override
+        public void closeChannelAndWriteFrame(final AMQChannel channel, final int cause, final String message)
+        {
+            _closedChannel = channel;
+            _channelCloseErrorCode = cause;
+            _channelCloseMessage = message;
+            _channelCloseCount++;
+            super.closeChannelAndWriteFrame(channel, cause, message);
+        }
+
+        private void recordConnectionClose(final int errorCode,
+                                           final int channelId,
+                                           final int classId,
+                                           final int methodId)
+        {
+            _closeErrorCode = errorCode;
+            _closeChannelId = channelId;
+            _closeClassId = classId;
+            _closeMethodId = methodId;
+            _closeCount++;
+        }
+
+        private int getCloseErrorCode()
+        {
+            return _closeErrorCode;
+        }
+
+        private int getCloseChannelId()
+        {
+            return _closeChannelId;
+        }
+
+        private int getCloseClassId()
+        {
+            return _closeClassId;
+        }
+
+        private int getCloseMethodId()
+        {
+            return _closeMethodId;
+        }
+
+        private int getCloseCount()
+        {
+            return _closeCount;
+        }
+
+        private AMQChannel getClosedChannel()
+        {
+            return _closedChannel;
+        }
+
+        private int getChannelCloseErrorCode()
+        {
+            return _channelCloseErrorCode;
+        }
+
+        private String getChannelCloseMessage()
+        {
+            return _channelCloseMessage;
+        }
+
+        private int getChannelCloseCount()
+        {
+            return _channelCloseCount;
+        }
+    }
+
+    private AMQPConnection_0_8Impl createConnectionAwaitingStartOk()
+    {
+        final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+                _protocol, 0, _ticker);
+        connection.create();
+        connection.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
+        return connection;
+    }
+
+    private RecordingAMQPConnection createOpenRecordingConnection(final ProtocolVersion protocolVersion,
+                                                                   final Protocol protocol)
+    {
+        final RecordingAMQPConnection connection = new RecordingAMQPConnection(_broker, _network, _port, _transport,
+                protocol, _ticker);
+        openConnection(connection, protocolVersion);
+        return connection;
+    }
+
+    private void openConnection(final AMQPConnection_0_8Impl connection, final ProtocolVersion protocolVersion)
+    {
+        connection.create();
+        connection.receiveProtocolHeader(new ProtocolInitiation(protocolVersion));
+        connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()), SASL_MECH, SASL_RESPONSE,
+                LOCALE);
+        connection.receiveConnectionTuneOk(10, 65535, 0);
+        connection.receiveConnectionOpen(AMQShortString.createAMQShortString(VIRTUAL_HOST_NAME),
+                AMQShortString.EMPTY_STRING, false);
+    }
 }
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java
index 8fd305f4df..2bd32f3e32 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java
@@ -22,6 +22,11 @@ package org.apache.qpid.server.protocol.v0_8;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -71,8 +76,189 @@ class EncodingUtilsTest extends UnitTestBase
         _buffer.put((byte)'1');
         _buffer.put((byte)'a');
         _buffer.flip();
-        assertThrows(AMQFrameDecodingException.class,
-                () -> EncodingUtils.readLongAsShortString(_buffer),
+        assertThrows(AMQFrameDecodingException.class, () -> EncodingUtils.readLongAsShortString(_buffer),
                 "Exception is expected");
     }
-}
\ No newline at end of file
+
+    @Test
+    void readLongStringReadsValidValue()
+    {
+        _buffer.putInt(2);
+        _buffer.put((byte) 'h');
+        _buffer.put((byte) 'i');
+        _buffer.flip();
+        assertEquals("hi", EncodingUtils.readLongString(_buffer), "Unexpected result");
+    }
+
+    @Test
+    void readLongStringRejectsUnsignedLengthLargerThanRemaining()
+    {
+        _buffer.putInt(0x80000000);
+        _buffer.put((byte) 'x');
+        _buffer.flip();
+        assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readLongString(_buffer));
+    }
+
+    @Test
+    void readLongStringRejectsOversizedLength()
+    {
+        _buffer.putInt(0x20000000);
+        _buffer.put((byte) 'x');
+        _buffer.flip();
+        assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readLongString(_buffer));
+    }
+
+    @Test
+    void readLongstrRejectsUnsignedLengthLargerThanRemaining()
+    {
+        _buffer.putInt(0x80000000);
+        _buffer.put((byte) 1);
+        _buffer.flip();
+        assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readLongstr(_buffer));
+    }
+
+    @Test
+    void readBytesRejectsOversizedLength()
+    {
+        _buffer.putInt(0x20000000);
+        _buffer.put((byte) 1);
+        _buffer.flip();
+        assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readBytes(_buffer));
+    }
+
+    @Test
+    void fieldArrayRejectsUnsignedLengthLargerThanRemaining()
+    {
+        _buffer.putInt(0x80000000);
+        _buffer.put((byte) 1);
+        _buffer.flip();
+
+        assertThrows(IllegalArgumentException.class, () -> FieldArray.readFromBuffer(_buffer));
+    }
+
+    @Test
+    void readFieldTableRejectsOversizedLengthBeforeCreatingView()
+    {
+        final QpidByteBuffer input = mock(QpidByteBuffer.class);
+        when(input.getUnsignedInt()).thenReturn(1L);
+        when(input.remaining()).thenReturn(0);
+
+        assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readFieldTable(input));
+
+        verify(input, never()).view(anyInt(), anyInt());
+        verify(input, never()).position(anyInt());
+    }
+
+    @Test
+    void fieldTableWithOversizedLongStringValueIsRejected()
+    {
+        _buffer.putUnsignedByte((short) 1);              // key length
+        _buffer.put((byte) 's');                         // key "s"
+        _buffer.put(AMQType.LONG_STRING.identifier());   // value type 'S'
+        _buffer.putInt(0x80000000);                      // long-string length
+        _buffer.flip();
+        final int length = _buffer.remaining();
+        final FieldTable fieldTable = FieldTableFactory.createFieldTable(_buffer, length);
+        try
+        {
+            assertThrows(IllegalArgumentException.class, fieldTable::validate);
+        }
+        finally
+        {
+            fieldTable.dispose();
+        }
+    }
+
+    @Test
+    void decodedFieldTableDetachesNestedTableFromPooledBuffer()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        QpidByteBuffer input = QpidByteBuffer.allocateDirect(60);
+        FieldTable encodedTable = null;
+        FieldTable decodedTable = null;
+        FieldTable nestedTable = null;
+        try
+        {
+            input.putInt(14);                                  // outer field-table length
+            input.putUnsignedByte((short) 1);
+            input.put((byte) 'n');
+            input.put(AMQType.FIELD_TABLE.identifier());
+            input.putInt(7);                                   // nested field-table length
+            input.putUnsignedByte((short) 1);
+            input.put((byte) 'x');
+            input.put(AMQType.INT.identifier());
+            input.putInt(42);
+            input.flip();
+
+            encodedTable = EncodingUtils.readFieldTable(input);
+            decodedTable = FieldTable.convertToDecodedFieldTable(encodedTable);
+            nestedTable = (FieldTable) decodedTable.get("n");
+
+            encodedTable.dispose();
+            encodedTable = null;
+            input.dispose();
+            input = null;
+
+            assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                    "Decoded nested field table retained a pooled direct buffer");
+            assertEquals(42, nestedTable.get("x"));
+        }
+        finally
+        {
+            if (nestedTable != null)
+            {
+                nestedTable.dispose();
+            }
+            if (decodedTable != null)
+            {
+                decodedTable.dispose();
+            }
+            if (encodedTable != null)
+            {
+                encodedTable.dispose();
+            }
+            if (input != null)
+            {
+                input.dispose();
+            }
+        }
+    }
+
+    @Test
+    void malformedFieldArrayAfterNestedTableDoesNotLeakPooledBuffer()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        QpidByteBuffer input = QpidByteBuffer.allocateDirect(60);
+        try
+        {
+            input.putInt(20);                                  // outer field-table length
+            input.putUnsignedByte((short) 1);
+            input.put((byte) 'a');
+            input.put(AMQType.FIELD_ARRAY.identifier());
+            input.putInt(13);                                  // field-array payload length
+            input.put(AMQType.FIELD_TABLE.identifier());
+            input.putInt(3);                                   // nested field-table length
+            input.putUnsignedByte((short) 1);
+            input.put((byte) 'x');
+            input.put(AMQType.VOID.identifier());
+            input.put(AMQType.LONG_STRING.identifier());
+            input.putInt(0x80000000);
+            input.flip();
+
+            final QpidByteBuffer tableToDecode = input;
+            assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readFieldTable(tableToDecode));
+            input.dispose();
+            input = null;
+
+            assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                    "Malformed field array leaked a nested field-table view");
+        }
+        finally
+        {
+            if (input != null)
+            {
+                input.dispose();
+            }
+        }
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableNestingDecoderTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableNestingDecoderTest.java
new file mode 100644
index 0000000000..21620952f0
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableNestingDecoderTest.java
@@ -0,0 +1,354 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import org.mockito.ArgumentCaptor;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicConsumeBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueBindBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class FieldTableNestingDecoderTest extends UnitTestBase
+{
+    private static final byte FRAME_END = (byte) 0xCE;
+    private static final byte METHOD_FRAME = 1;
+    private static final int CHANNEL_ID = 1;
+    private static final int CONTENT_HEADERS_PROPERTY_FLAG = 1 << 13;
+    private static final int CONFIGURED_MAXIMUM = 4;
+    private static final int CONFIGURED_MAXIMUM_ABOVE_DEFAULT = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+
+    @ParameterizedTest(name = "{0} accepts {1} arguments at the nesting limit")
+    @MethodSource("protocolVersionsAndMethods")
+    void methodArgumentsAtLimitAreAccepted(final ProtocolVersion protocolVersion,
+                                           final FieldTableMethod method)
+            throws Exception
+    {
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(channelMethodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+                createMethodProcessor(protocolVersion, channelMethodProcessor);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer frame = createMethodFrame(method, buildNestedTable(CONFIGURED_MAXIMUM)))
+        {
+            decoder.decodeBuffer(frame);
+        }
+
+        verify(channelMethodProcessor).ignoreAllButCloseOk();
+    }
+
+    @ParameterizedTest(name = "{0} preserves a configured limit above the default after dispatch")
+    @MethodSource("protocolVersions")
+    void configuredLimitAboveDefaultIsPreservedAfterDispatch(final ProtocolVersion protocolVersion)
+            throws Exception
+    {
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+                createMethodProcessor(protocolVersion, channelMethodProcessor);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM_ABOVE_DEFAULT);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer frame = createMethodFrame(FieldTableMethod.QUEUE_DECLARE,
+                buildNestedTable(CONFIGURED_MAXIMUM_ABOVE_DEFAULT)))
+        {
+            decoder.decodeBuffer(frame);
+        }
+
+        final ArgumentCaptor<FieldTable> argumentsCaptor = ArgumentCaptor.forClass(FieldTable.class);
+        verify(channelMethodProcessor).receiveQueueDeclare(any(), eq(false), eq(false), eq(false), eq(false),
+                eq(false), argumentsCaptor.capture());
+        final FieldTable arguments = argumentsCaptor.getValue();
+        try
+        {
+            final FieldTable nested = (FieldTable) arguments.get("n");
+            try
+            {
+                assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+            }
+            finally
+            {
+                nested.dispose();
+            }
+        }
+        finally
+        {
+            arguments.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "{0} rejects over-limit {1} arguments")
+    @MethodSource("protocolVersionsAndMethods")
+    void methodArgumentsOverLimitAreRejectedBeforeDispatch(final ProtocolVersion protocolVersion,
+                                                           final FieldTableMethod method)
+    {
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+                createMethodProcessor(protocolVersion, channelMethodProcessor);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer frame = createMethodFrame(method, buildNestedTable(CONFIGURED_MAXIMUM + 1)))
+        {
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(frame));
+        }
+
+        final int classAndMethod = method.getClassAndMethod();
+        assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+        assertEquals(classAndMethod >> 16, exception.getClassId());
+        assertEquals(classAndMethod & 0xFFFF, exception.getMethodId());
+        verify(channelMethodProcessor, never()).ignoreAllButCloseOk();
+    }
+
+    @ParameterizedTest(name = "{0} accepts content headers at the nesting limit")
+    @MethodSource("protocolVersions")
+    void contentHeadersAtLimitAreAccepted(final ProtocolVersion protocolVersion) throws Exception
+    {
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(channelMethodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+                createMethodProcessor(protocolVersion, channelMethodProcessor);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer frame = createContentHeaderFrame(buildNestedTable(CONFIGURED_MAXIMUM)))
+        {
+            decoder.decodeBuffer(frame);
+        }
+
+        verify(channelMethodProcessor).ignoreAllButCloseOk();
+    }
+
+    @ParameterizedTest(name = "{0} rejects over-limit content headers")
+    @MethodSource("protocolVersions")
+    void contentHeadersOverLimitAreRejectedBeforeDispatch(final ProtocolVersion protocolVersion)
+    {
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+                createMethodProcessor(protocolVersion, channelMethodProcessor);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer frame = createContentHeaderFrame(buildNestedTable(CONFIGURED_MAXIMUM + 1)))
+        {
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(frame));
+        }
+
+        assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+        assertEquals(0, exception.getClassId());
+        assertEquals(0, exception.getMethodId());
+        verify(channelMethodProcessor, never()).ignoreAllButCloseOk();
+    }
+
+    @Test
+    void brokerDecoderUsesProtocolSpecificNestingContext()
+    {
+        final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+        when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS))
+                .thenReturn(CONFIGURED_MAXIMUM);
+        final BrokerDecoder decoder = new BrokerDecoder(connection);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer frame = createConnectionStartOkFrame(buildNestedTable(CONFIGURED_MAXIMUM + 1)))
+        {
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(frame));
+        }
+
+        assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+        assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+        assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+        verify(connection).getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS);
+        verify(connection, never()).receiveConnectionStartOk(any(), any(), any(), any());
+    }
+
+    @Test
+    void negativeProtocolSpecificNestingContextIsRejected()
+    {
+        final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+        when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS)).thenReturn(-1);
+
+        assertThrows(IllegalArgumentException.class, () -> new BrokerDecoder(connection));
+    }
+
+    private static Stream<Arguments> protocolVersionsAndMethods()
+    {
+        return protocolVersions().flatMap(protocolVersion -> Stream.of(FieldTableMethod.values())
+                .map(method -> Arguments.of(protocolVersion, method)));
+    }
+
+    private static Stream<ProtocolVersion> protocolVersions()
+    {
+        return Stream.of(ProtocolVersion.v0_8, ProtocolVersion.v0_9, ProtocolVersion.v0_91);
+    }
+
+    private static ServerMethodProcessor<ServerChannelMethodProcessor> createMethodProcessor(
+            final ProtocolVersion protocolVersion,
+            final ServerChannelMethodProcessor channelMethodProcessor)
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+                mock(ServerMethodProcessor.class);
+        when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+        return methodProcessor;
+    }
+
+    private static QpidByteBuffer createMethodFrame(final FieldTableMethod method, final byte[] fieldTable)
+    {
+        final int argumentsSize = switch (method)
+        {
+            case QUEUE_DECLARE -> Short.BYTES + 1 + 1;
+            case QUEUE_BIND -> Short.BYTES + 1 + 1 + 1 + 1;
+            case EXCHANGE_DECLARE, BASIC_CONSUME -> Short.BYTES + 1 + 1 + 1;
+        };
+
+        final int bodySize = Integer.BYTES + argumentsSize + Integer.BYTES + fieldTable.length;
+        final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+        writeFrameHeader(frame, METHOD_FRAME, CHANNEL_ID, bodySize);
+        frame.putInt(method.getClassAndMethod());
+        frame.putUnsignedShort(0);
+        frame.put((byte) 0);
+        if (method != FieldTableMethod.QUEUE_DECLARE)
+        {
+            frame.put((byte) 0);
+        }
+        if (method == FieldTableMethod.QUEUE_BIND)
+        {
+            frame.put((byte) 0);
+        }
+        frame.put((byte) 0);
+        writeFieldTable(frame, fieldTable);
+        frame.put(FRAME_END);
+        frame.flip();
+        return frame;
+    }
+
+    private static QpidByteBuffer createContentHeaderFrame(final byte[] fieldTable)
+    {
+        final int bodySize = 2 * Short.BYTES + Long.BYTES + Short.BYTES + Integer.BYTES + fieldTable.length;
+        final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+        writeFrameHeader(frame, ContentHeaderBody.TYPE, CHANNEL_ID, bodySize);
+        frame.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+        frame.putUnsignedShort(0);
+        frame.putLong(0);
+        frame.putUnsignedShort(CONTENT_HEADERS_PROPERTY_FLAG);
+        writeFieldTable(frame, fieldTable);
+        frame.put(FRAME_END);
+        frame.flip();
+        return frame;
+    }
+
+    private static QpidByteBuffer createConnectionStartOkFrame(final byte[] fieldTable)
+    {
+        final int bodySize = Integer.BYTES + Integer.BYTES + fieldTable.length + 1 + Integer.BYTES + 1;
+        final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+        writeFrameHeader(frame, METHOD_FRAME, 0, bodySize);
+        frame.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+        writeFieldTable(frame, fieldTable);
+        frame.put((byte) 0);
+        frame.putUnsignedInt(0);
+        frame.put((byte) 0);
+        frame.put(FRAME_END);
+        frame.flip();
+        return frame;
+    }
+
+    private static void writeFrameHeader(final QpidByteBuffer buffer,
+                                         final byte type,
+                                         final int channel,
+                                         final long bodySize)
+    {
+        buffer.put(type);
+        buffer.putUnsignedShort(channel);
+        buffer.putUnsignedInt(bodySize);
+    }
+
+    private static void writeFieldTable(final QpidByteBuffer buffer, final byte[] fieldTable)
+    {
+        buffer.putUnsignedInt(fieldTable.length);
+        buffer.put(fieldTable);
+    }
+
+    private static byte[] buildNestedTable(final int depth)
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+        for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+        {
+            buffer.put((byte) 1);
+            buffer.put((byte) 'n');
+            buffer.put(AMQType.FIELD_TABLE.identifier());
+            buffer.putInt(7 * (remainingDepth - 1));
+        }
+        buffer.put((byte) 1);
+        buffer.put((byte) 'v');
+        buffer.put(AMQType.INT.identifier());
+        buffer.putInt(42);
+        return buffer.array();
+    }
+
+    private enum FieldTableMethod
+    {
+        QUEUE_DECLARE,
+        QUEUE_BIND,
+        EXCHANGE_DECLARE,
+        BASIC_CONSUME;
+
+        int getClassAndMethod()
+        {
+            return switch (this)
+            {
+                case QUEUE_DECLARE -> (QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID;
+                case QUEUE_BIND -> (QueueBindBody.CLASS_ID << 16) | QueueBindBody.METHOD_ID;
+                case EXCHANGE_DECLARE -> (ExchangeDeclareBody.CLASS_ID << 16) | ExchangeDeclareBody.METHOD_ID;
+                case BASIC_CONSUME -> (BasicConsumeBody.CLASS_ID << 16) | BasicConsumeBody.METHOD_ID;
+            };
+        }
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java
index 6298fcacc6..03f3bbf999 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java
@@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
@@ -35,6 +36,8 @@ import java.util.List;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.test.utils.UnitTestBase;
@@ -414,8 +417,7 @@ class FieldTableTest extends UnitTestBase
     void checkPropertyNameIsNull()
     {
         final Map<String, Object> map = Collections.singletonMap(null, "String");
-        assertThrows(IllegalArgumentException.class,
-                () -> FieldTableFactory.createFieldTable(map),
+        assertThrows(IllegalArgumentException.class, () -> FieldTableFactory.createFieldTable(map),
                 "Null property name is not allowed");
     }
 
@@ -426,8 +428,7 @@ class FieldTableTest extends UnitTestBase
     void checkPropertyNameIsEmptyString()
     {
         final Map<String, Object> map =Map.of("", "String");
-        assertThrows(IllegalArgumentException.class,
-                () -> FieldTableFactory.createFieldTable(map),
+        assertThrows(IllegalArgumentException.class, () -> FieldTableFactory.createFieldTable(map),
                 "Empty property name is not allowed");
 
     }
@@ -441,9 +442,8 @@ class FieldTableTest extends UnitTestBase
         final boolean strictAMQP = FieldTable._strictAMQP;
         final Map<String, Object> map = Map.of("x".repeat(129), "String");
         FieldTable._strictAMQP = true;
-        assertThrows(IllegalArgumentException.class,
-                     () -> new FieldTable(map),
-                     "property name must be < 128 characters");
+        assertThrows(IllegalArgumentException.class, () -> new FieldTable(map),
+                "property name must be < 128 characters");
         FieldTable._strictAMQP = strictAMQP;
     }
 
@@ -458,9 +458,8 @@ class FieldTableTest extends UnitTestBase
         // Try a name that starts with a number
         FieldTable._strictAMQP = true;
         final Map<String, Object> map = Map.of("1", "String");
-        assertThrows(IllegalArgumentException.class,
-                     () -> new FieldTable(map),
-                     "property name must start with a letter");
+        assertThrows(IllegalArgumentException.class, () -> new FieldTable(map),
+                "property name must start with a letter");
         FieldTable._strictAMQP = strictAMQP;
     }
 
@@ -501,6 +500,356 @@ class FieldTableTest extends UnitTestBase
         assertTrue(fieldTable.containsKey("testKey"), "Expected key is not found");
     }
 
+    @Test
+    void malformedLongStringInNestedTableIsRejectedDuringRead()
+    {
+        final byte[] malformedNestedTable = tableEntryWithLongStringLength(0x80000000);
+        final byte[] outerTable = tableEntryWithCompoundValue(AMQType.FIELD_TABLE, malformedNestedTable);
+
+        assertThrows(IllegalArgumentException.class, () -> readFieldTable(outerTable));
+    }
+
+    @Test
+    void malformedLongStringInTableNestedInArrayIsRejectedDuringRead()
+    {
+        final byte[] malformedNestedTable = tableEntryWithLongStringLength(0x80000000);
+        final byte[] array = compoundValue(AMQType.FIELD_TABLE, malformedNestedTable);
+        final byte[] outerTable = tableEntryWithCompoundValue(AMQType.FIELD_ARRAY, array);
+
+        assertThrows(IllegalArgumentException.class, () -> readFieldTable(outerTable));
+    }
+
+    @Test
+    void maximumNestingDepthCanBeDetachedAndTraversed()
+    {
+        final FieldTable encoded =
+                readFieldTable(buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+        final FieldTable decoded = FieldTable.convertToDecodedFieldTable(encoded);
+        try
+        {
+            FieldTable current = decoded;
+            for (int i = 1; i < AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS; i++)
+            {
+                current = (FieldTable) current.get("n");
+            }
+            assertEquals(42, current.get("v"));
+        }
+        finally
+        {
+            encoded.dispose();
+            decoded.dispose();
+        }
+    }
+
+    @Test
+    void maximumNestingDepthCanBeConverted()
+    {
+        final FieldTable fieldTable =
+                readFieldTable(buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+        try
+        {
+            assertDoesNotThrow(() -> FieldTable.convertToMap(fieldTable));
+        }
+        finally
+        {
+            fieldTable.dispose();
+        }
+    }
+
+    @Test
+    void excessiveNestingIsRejectedDuringReadWithoutStackOverflow()
+    {
+        assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedTable(
+                AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 1)));
+        assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedTable(10_000)));
+    }
+
+    @Test
+    void excessiveProgrammaticNestingIsRejectedDuringConversionWithoutStackOverflow()
+    {
+        final FieldTable fieldTable = buildProgrammaticNestedTable(10_000);
+
+        assertThrows(AMQValueNestingException.class, () -> FieldTable.convertToMap(fieldTable));
+    }
+
+    @Test
+    void maximumArrayNestingDepthCanBeDecoded()
+    {
+        final int arrayDepth = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS - 1;
+        final FieldTable fieldTable = readFieldTable(buildNestedArrayTable(arrayDepth));
+        try
+        {
+            Object current = fieldTable.get("n");
+            for (int i = 1; i < arrayDepth; i++)
+            {
+                current = ((Collection<?>) current).iterator().next();
+            }
+            assertEquals(42, ((Collection<?>) current).iterator().next());
+        }
+        finally
+        {
+            fieldTable.dispose();
+        }
+    }
+
+    @Test
+    void excessiveArrayNestingIsRejectedDuringReadWithoutStackOverflow()
+    {
+        assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedArrayTable(
+                AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS)));
+        assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedArrayTable(10_000)));
+    }
+
+    @Test
+    void configuredNestingDepthAppliesToTablesAndArrays()
+    {
+        final int configuredLimit = 2;
+        FieldTable table = null;
+        FieldTable array = null;
+        try
+        {
+            table = readFieldTable(buildNestedTable(configuredLimit), configuredLimit);
+            array = readFieldTable(buildNestedArrayTable(configuredLimit - 1), configuredLimit);
+            assertNotNull(table);
+            assertNotNull(array);
+        }
+        finally
+        {
+            if (table != null)
+            {
+                table.dispose();
+            }
+            if (array != null)
+            {
+                array.dispose();
+            }
+        }
+
+        assertThrows(AMQValueNestingException.class, () ->
+                readFieldTable(buildNestedTable(configuredLimit + 1), configuredLimit));
+        assertThrows(AMQValueNestingException.class, () ->
+                readFieldTable(buildNestedArrayTable(configuredLimit), configuredLimit));
+    }
+
+    @Test
+    void configuredLimitAboveDefaultIsPreservedDuringConversion()
+    {
+        final int configuredLimit = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+        final FieldTable encoded = readFieldTable(buildNestedTable(configuredLimit), configuredLimit);
+        final FieldTable decoded = FieldTable.convertToDecodedFieldTable(encoded);
+        try
+        {
+            assertDoesNotThrow(() -> encoded.validate());
+            assertDoesNotThrow(() -> FieldTable.convertToMap(decoded));
+            final FieldTable nested = (FieldTable) decoded.get("n");
+            try
+            {
+                assertThrows(AMQValueNestingException.class, () ->
+                        FieldTable.convertToMap(nested, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+                assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+            }
+            finally
+            {
+                nested.dispose();
+            }
+        }
+        finally
+        {
+            decoded.dispose();
+            encoded.dispose();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void configuredLimitAboveDefaultIsPreservedByLazyNestedTable(final boolean direct)
+    {
+        final int configuredLimit = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+        final FieldTable fieldTable = readFieldTable(buildNestedTable(configuredLimit), configuredLimit, direct);
+        try
+        {
+            final FieldTable nested = (FieldTable) fieldTable.get("n");
+            try
+            {
+                assertThrows(AMQValueNestingException.class, () ->
+                        FieldTable.convertToMap(nested, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+                assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+            }
+            finally
+            {
+                nested.dispose();
+            }
+        }
+        finally
+        {
+            fieldTable.dispose();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void configuredLimitAboveDefaultIsPreservedByTableNestedInArray(final boolean direct)
+    {
+        final int configuredLimit = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+        final byte[] payload = buildNestedTableInArray(configuredLimit - 2);
+        final FieldTable fieldTable = readFieldTable(payload, configuredLimit, direct);
+        try
+        {
+            final Collection<?> array = (Collection<?>) fieldTable.get("n");
+            final FieldTable nested = (FieldTable) array.iterator().next();
+            try
+            {
+                assertThrows(AMQValueNestingException.class, () ->
+                        FieldTable.convertToMap(nested, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+                assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+            }
+            finally
+            {
+                nested.dispose();
+            }
+        }
+        finally
+        {
+            fieldTable.dispose();
+        }
+    }
+
+    @Test
+    void stricterValidationIsAppliedAfterSuccessfulRead()
+    {
+        final FieldTable fieldTable = readFieldTable(buildNestedTable(3), 3);
+        try
+        {
+            assertThrows(AMQValueNestingException.class, () -> fieldTable.validate(2));
+        }
+        finally
+        {
+            fieldTable.dispose();
+        }
+    }
+
+    @Test
+    void malformedLongValueLengthIsRejectedDuringNestingValidation()
+    {
+        for (final AMQType type : List.of(AMQType.LONG_STRING, AMQType.BINARY, AMQType.ASCII_STRING, AMQType.WIDE_STRING))
+        {
+            final ByteBuffer buffer = ByteBuffer.allocate(7);
+            buffer.put((byte) 1);
+            buffer.put((byte) 'v');
+            buffer.put(type.identifier());
+            buffer.putInt(-1);
+
+            assertThrows(IllegalArgumentException.class, () -> readFieldTable(buffer.array(), 1), type.name());
+        }
+    }
+
+    private static FieldTable readFieldTable(final byte[] payload)
+    {
+        return readFieldTable(payload, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+    }
+
+    private static FieldTable readFieldTable(final byte[] payload, final int maxNestedObjects)
+    {
+        return readFieldTable(payload, maxNestedObjects, false);
+    }
+
+    private static FieldTable readFieldTable(final byte[] payload,
+                                             final int maxNestedObjects,
+                                             final boolean direct)
+    {
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(direct, Integer.BYTES + payload.length))
+        {
+            buffer.putUnsignedInt(payload.length);
+            buffer.put(payload);
+            buffer.flip();
+            return EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+        }
+    }
+
+    private static byte[] buildNestedTable(final int depth)
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+        for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+        {
+            buffer.put((byte) 1);
+            buffer.put((byte) 'n');
+            buffer.put(AMQType.FIELD_TABLE.identifier());
+            buffer.putInt(7 * (remainingDepth - 1));
+        }
+        buffer.put((byte) 1);
+        buffer.put((byte) 'v');
+        buffer.put(AMQType.INT.identifier());
+        buffer.putInt(42);
+        return buffer.array();
+    }
+
+    private static byte[] buildNestedArrayTable(final int arrayDepth)
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(7 + 5 * arrayDepth);
+        buffer.put((byte) 1);
+        buffer.put((byte) 'n');
+        buffer.put(AMQType.FIELD_ARRAY.identifier());
+        buffer.putInt(5 * arrayDepth);
+        for (int remainingDepth = arrayDepth; remainingDepth > 1; remainingDepth--)
+        {
+            buffer.put(AMQType.FIELD_ARRAY.identifier());
+            buffer.putInt(5 * (remainingDepth - 1));
+        }
+        buffer.put(AMQType.INT.identifier());
+        buffer.putInt(42);
+        return buffer.array();
+    }
+
+    private static byte[] buildNestedTableInArray(final int tableDepth)
+    {
+        final byte[] nestedTable = buildNestedTable(tableDepth);
+        final ByteBuffer array = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES + nestedTable.length);
+        array.put(AMQType.FIELD_TABLE.identifier());
+        array.putInt(nestedTable.length);
+        array.put(nestedTable);
+        return tableEntryWithCompoundValue(AMQType.FIELD_ARRAY, array.array());
+    }
+
+    private static byte[] tableEntryWithLongStringLength(final int length)
+    {
+        return ByteBuffer.allocate(7)
+                .put((byte) 1)
+                .put((byte) 's')
+                .put(AMQType.LONG_STRING.identifier())
+                .putInt(length)
+                .array();
+    }
+
+    private static byte[] tableEntryWithCompoundValue(final AMQType type, final byte[] value)
+    {
+        return ByteBuffer.allocate(7 + value.length)
+                .put((byte) 1)
+                .put((byte) 'n')
+                .put(type.identifier())
+                .putInt(value.length)
+                .put(value)
+                .array();
+    }
+
+    private static byte[] compoundValue(final AMQType type, final byte[] value)
+    {
+        return ByteBuffer.allocate(5 + value.length)
+                .put(type.identifier())
+                .putInt(value.length)
+                .put(value)
+                .array();
+    }
+
+    private static FieldTable buildProgrammaticNestedTable(final int depth)
+    {
+        FieldTable fieldTable = FieldTableFactory.createFieldTable(Map.of("v", 42));
+        for (int currentDepth = 1; currentDepth < depth; currentDepth++)
+        {
+            fieldTable = FieldTableFactory.createFieldTable(Map.of("n", fieldTable));
+        }
+        return fieldTable;
+    }
+
     private FieldTable buildMalformedFieldTable()
     {
         final QpidByteBuffer buf = QpidByteBuffer.allocate(1);
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/IncomingMessageTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/IncomingMessageTest.java
new file mode 100644
index 0000000000..4aab09ee73
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/IncomingMessageTest.java
@@ -0,0 +1,125 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicContentHeaderProperties;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.MessagePublishInfo;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class IncomingMessageTest extends UnitTestBase
+{
+    @Test
+    void emptyContentBodyFramesAreCountedButNotRetained()
+    {
+        final IncomingMessage message = createMessage(1L);
+
+        try (final QpidByteBuffer emptyContent = QpidByteBuffer.wrap(new byte[0]))
+        {
+            for (int i = 0; i < 100_000; i++)
+            {
+                message.addContentBodyFrame(emptyContent);
+            }
+        }
+
+        assertEquals(100_000, message.getContentBodyFrameCount());
+        assertEquals(0, message.getContentChunkCount());
+        assertFalse(message.allContentReceived());
+        message.getContentHeader().dispose();
+    }
+
+    @Test
+    void nonEmptyContentBodyFrameIsRetainedOnce()
+    {
+        final IncomingMessage message = createMessage(1L);
+
+        try (final QpidByteBuffer content = QpidByteBuffer.wrap(new byte[]{42}))
+        {
+            assertTrue(message.addContentBodyFrame(content));
+        }
+
+        assertEquals(1, message.getContentBodyFrameCount());
+        assertEquals(1, message.getContentChunkCount());
+        assertTrue(message.allContentReceived());
+
+        final QpidByteBuffer retainedContent = message.getContentChunk(0);
+        assertEquals((byte) 42, retainedContent.get());
+        retainedContent.dispose();
+        message.getContentHeader().dispose();
+    }
+
+    @Test
+    void contentBodyFrameExceedingDeclaredSizeIsNotRetained()
+    {
+        final IncomingMessage message = createMessage(1L);
+
+        try (final QpidByteBuffer content = QpidByteBuffer.wrap(new byte[2]))
+        {
+            assertFalse(message.addContentBodyFrame(content));
+        }
+
+        assertEquals(0, message.getContentBodyFrameCount());
+        assertEquals(0, message.getContentChunkCount());
+        assertFalse(message.allContentReceived());
+        message.getContentHeader().dispose();
+    }
+
+    @Test
+    void disposeReleasesHeaderAndRetainedContent()
+    {
+        final IncomingMessage message = new IncomingMessage(mock(MessagePublishInfo.class));
+        final ContentHeaderBody contentHeader = mock(ContentHeaderBody.class);
+        when(contentHeader.getBodySize()).thenReturn(1L);
+        message.setContentHeaderBody(contentHeader);
+        final QpidByteBuffer content = mock(QpidByteBuffer.class);
+        final QpidByteBuffer retainedContent = mock(QpidByteBuffer.class);
+        when(content.remaining()).thenReturn(1);
+        when(content.duplicate()).thenReturn(retainedContent);
+
+        assertTrue(message.addContentBodyFrame(content));
+
+        message.dispose();
+        message.dispose();
+
+        verify(contentHeader).dispose();
+        verify(retainedContent).dispose();
+        assertNull(message.getContentHeader());
+        assertEquals(0, message.getContentChunkCount());
+    }
+
+    private IncomingMessage createMessage(final long bodySize)
+    {
+        final IncomingMessage message = new IncomingMessage(mock(MessagePublishInfo.class));
+        message.setContentHeaderBody(new ContentHeaderBody(new BasicContentHeaderProperties(), bodySize));
+        return message;
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/ServerDecoderTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/ServerDecoderTest.java
new file mode 100644
index 0000000000..261dbcd886
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/ServerDecoderTest.java
@@ -0,0 +1,1103 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicAckBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicPublishBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionSecureOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
+import org.apache.qpid.server.protocol.v0_8.transport.HeartbeatBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.TxSelectBody;
+import org.apache.qpid.server.store.StoreException;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ServerDecoderTest extends UnitTestBase
+{
+    private static final byte FRAME_END = (byte) 0xCE;
+    private static final int CHANNEL_ID = 1;
+
+    @ParameterizedTest
+    @MethodSource("rejectedFrameReadVariants")
+    void testRejectedFrameIsConsumedBeforeCloseOk(final ProtocolVersion protocolVersion,
+                                                  final int classAndMethod,
+                                                  final int firstReadLength,
+                                                  final boolean direct) throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+        final ServerDecoder decoder = createRejectingDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final int frameSize = AMQDecoder.FRAME_HEADER_SIZE + Integer.BYTES + 1;
+
+        try (final QpidByteBuffer frames = QpidByteBuffer.allocate(frameSize * 2);
+             final QpidByteBuffer buffer = QpidByteBuffer.allocate(direct, frameSize * 2))
+        {
+            writeFrameHeader(frames, (byte) 1, CHANNEL_ID, Integer.BYTES);
+            frames.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            frames.put(FRAME_END);
+            writeFrameHeader(frames, (byte) 1, 0, Integer.BYTES);
+            frames.putInt(classAndMethod);
+            frames.put(FRAME_END);
+            frames.flip();
+            final byte[] input = new byte[frames.remaining()];
+            frames.get(input);
+
+            buffer.put(input, 0, firstReadLength);
+            buffer.flip();
+            if (firstReadLength < frameSize)
+            {
+                decoder.decodeBuffer(buffer);
+                assertEquals(0, buffer.position(), "Incomplete frames must remain buffered");
+                buffer.compact();
+                buffer.put(input, firstReadLength, input.length - firstReadLength);
+                buffer.flip();
+            }
+
+            assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+            assertEquals(frameSize, buffer.position(), "Rejected frame must be completely consumed");
+            when(methodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+
+            if (firstReadLength >= frameSize && firstReadLength < input.length)
+            {
+                buffer.compact();
+                buffer.put(input, firstReadLength, input.length - firstReadLength);
+                buffer.flip();
+            }
+
+            decoder.decodeBuffer(buffer);
+            assertFalse(buffer.hasRemaining());
+            verify(methodProcessor).receiveConnectionCloseOk();
+        }
+    }
+
+    @Test
+    void fieldLengthCannotConsumeFollowingFrame() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(24))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, 8);
+            buffer.putInt((ConnectionSecureOkBody.CLASS_ID << 16) | ConnectionSecureOkBody.METHOD_ID);
+            buffer.putUnsignedInt(4L);
+            buffer.put(FRAME_END);
+
+            writeFrameHeader(buffer, HeartbeatBody.TYPE, 0, 0);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        assertEquals(ConnectionSecureOkBody.CLASS_ID, exception.getClassId());
+        assertEquals(ConnectionSecureOkBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor, never()).receiveConnectionSecureOk(any(byte[].class));
+    }
+
+    @Test
+    void connectionClassMethodOnNonZeroChannelIsRejectedBeforeDispatch() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final int bodySize = Integer.BYTES + Short.BYTES + Integer.BYTES + Short.BYTES;
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, bodySize);
+            buffer.putInt((ConnectionTuneOkBody.CLASS_ID << 16) | ConnectionTuneOkBody.METHOD_ID);
+            buffer.putUnsignedShort(0);
+            buffer.putUnsignedInt(AMQDecoder.FRAME_MIN_SIZE);
+            buffer.putUnsignedShort(0);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.COMMAND_INVALID, exception.getErrorCode());
+        assertEquals(ConnectionTuneOkBody.CLASS_ID, exception.getClassId());
+        assertEquals(ConnectionTuneOkBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor, never()).receiveConnectionTuneOk(anyInt(), anyLong(), anyInt());
+        verify(methodProcessor, never()).setCurrentMethod(ConnectionTuneOkBody.CLASS_ID, ConnectionTuneOkBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+        verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+    }
+
+    @Test
+    void truncatedMethodBodyIsReportedWithFailingMethodIdentifiers() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + Integer.BYTES + 1))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+            buffer.putInt((BasicQosBody.CLASS_ID << 16) | BasicQosBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        assertEquals(BasicQosBody.CLASS_ID, exception.getClassId());
+        assertEquals(BasicQosBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+    }
+
+    @ParameterizedTest
+    @MethodSource("heartbeatChannelErrorCodes")
+    void heartbeatOnNonZeroChannelIsRejectedBeforeDispatch(final ProtocolVersion protocolVersion,
+                                                           final int expectedErrorCode) throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 1))
+        {
+            writeHeartbeatFrame(buffer, CHANNEL_ID, new byte[0]);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(expectedErrorCode, exception.getErrorCode());
+        verify(methodProcessor, never()).receiveHeartbeat();
+        verify(methodProcessor).getProtocolVersion();
+    }
+
+    @Test
+    void amqp091HeartbeatWithPayloadIsRejectedBeforeDispatch() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 2))
+        {
+            writeHeartbeatFrame(buffer, 0, new byte[] { 1 });
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        verify(methodProcessor, never()).receiveHeartbeat();
+    }
+
+    @ParameterizedTest
+    @MethodSource("legacyProtocolVersions")
+    void legacyHeartbeatWithPayloadRemainsCompatible(final ProtocolVersion protocolVersion) throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 2))
+        {
+            writeHeartbeatFrame(buffer, 0, new byte[] { 1 });
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        verify(methodProcessor).receiveHeartbeat();
+    }
+
+    @Test
+    void validHeartbeatDoesNotRequireProtocolVersionLookup() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 1))
+        {
+            writeHeartbeatFrame(buffer, 0, new byte[0]);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        verify(methodProcessor).receiveHeartbeat();
+        verify(methodProcessor, never()).getProtocolVersion();
+    }
+
+    @Test
+    void malformedNestedFieldTableIsRejectedBeforeLiveConnectionAccess() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final byte[] fieldTable = malformedNestedFieldTable();
+        final int bodySize = Integer.BYTES + Integer.BYTES + fieldTable.length + 6 + Integer.BYTES + 6;
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, bodySize);
+            buffer.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+            buffer.putUnsignedInt(fieldTable.length);
+            buffer.put(fieldTable);
+            writeShortString(buffer, "PLAIN");
+            buffer.putUnsignedInt(0);
+            writeShortString(buffer, "en_US");
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+        assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor, never()).ignoreAllButCloseOk();
+        verify(methodProcessor, never()).receiveConnectionStartOk(any(), any(), any(), any());
+        verify(methodProcessor, never()).setCurrentMethod(ConnectionStartOkBody.CLASS_ID, ConnectionStartOkBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+        verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+    }
+
+    @Test
+    void malformedNestedQueueArgumentsAreRejectedBeforeChannelLookup() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final byte[] fieldTable = malformedNestedFieldTable();
+        final int bodySize = Integer.BYTES + Short.BYTES + 1 + 1 + Integer.BYTES + fieldTable.length;
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, bodySize);
+            buffer.putInt((QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID);
+            buffer.putUnsignedShort(0);
+            writeShortString(buffer, "");
+            buffer.put((byte) 1);
+            buffer.putUnsignedInt(fieldTable.length);
+            buffer.put(fieldTable);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        assertEquals(QueueDeclareBody.CLASS_ID, exception.getClassId());
+        assertEquals(QueueDeclareBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+        verify(methodProcessor, never()).setCurrentMethod(QueueDeclareBody.CLASS_ID, QueueDeclareBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+        verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+    }
+
+    @Test
+    void configuredNestingDepthRejectsMethodBeforeDispatch() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, 1);
+        decoder.setExpectProtocolInitiation(false);
+        final byte[] fieldTable = nestedFieldTable(2);
+        final int bodySize = 24 + fieldTable.length;
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeConnectionStartOkFrame(buffer, fieldTable, bodySize);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+        assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+        assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor, never()).receiveConnectionStartOk(any(), any(), any(), any());
+        verify(methodProcessor, never()).setCurrentMethod(ConnectionStartOkBody.CLASS_ID,
+                ConnectionStartOkBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @Test
+    void configuredNestingDepthAcceptsMethodAtLimit() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor, 2);
+        decoder.setExpectProtocolInitiation(false);
+        final byte[] fieldTable = nestedFieldTable(2);
+        final int bodySize = 24 + fieldTable.length;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeConnectionStartOkFrame(buffer, fieldTable, bodySize);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        verify(methodProcessor).receiveConnectionStartOk(any(), any(), any(), any());
+        verify(methodProcessor).setCurrentMethod(ConnectionStartOkBody.CLASS_ID, ConnectionStartOkBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @Test
+    void brokerDecoderUsesProtocolSpecificNestingContext() throws Exception
+    {
+        final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+        when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS))
+                .thenReturn(1);
+        final BrokerDecoder decoder = new BrokerDecoder(connection);
+        decoder.setExpectProtocolInitiation(false);
+        final byte[] fieldTable = nestedFieldTable(2);
+        final int bodySize = 24 + fieldTable.length;
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeConnectionStartOkFrame(buffer, fieldTable, bodySize);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+        assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+        assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+        verify(connection).getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS);
+        verify(connection, never()).receiveConnectionStartOk(any(), any(), any(), any());
+    }
+
+    @Test
+    void negativeProtocolSpecificNestingContextIsRejected()
+    {
+        final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+        when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS))
+                .thenReturn(-1);
+
+        assertThrows(IllegalArgumentException.class, () -> new BrokerDecoder(connection));
+    }
+
+    @Test
+    void methodFrameWithTrailingBodyBytesIsRejected() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(13))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 5);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        verify(channelMethodProcessor, never()).receiveTxSelect();
+        verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+        verify(methodProcessor, never()).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+        verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+    }
+
+    @Test
+    void methodFrameWithInvalidEndMarkerIsRejectedBeforeDispatch() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 4);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put((byte) 0);
+            buffer.flip();
+
+            assertThrows(AMQFatalFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        verify(channelMethodProcessor, never()).receiveTxSelect();
+        verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+    }
+
+    @Test
+    void unsupportedFrameTypeIsFatal() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(2 * (AMQDecoder.FRAME_HEADER_SIZE + 1)))
+        {
+            writeFrameHeader(buffer, HeartbeatBody.TYPE, 0, 0);
+            buffer.put(FRAME_END);
+            writeFrameHeader(buffer, Byte.MAX_VALUE, 0, 0);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            assertThrows(AMQFatalFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        verify(methodProcessor).receiveHeartbeat();
+    }
+
+    @Test
+    void validMethodFrameIsDispatchedAfterValidation() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 4);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        final InOrder inOrder = inOrder(methodProcessor, channelMethodProcessor);
+        inOrder.verify(methodProcessor).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+        inOrder.verify(methodProcessor).getChannelMethodProcessor(CHANNEL_ID);
+        inOrder.verify(channelMethodProcessor).rejectMethodFrameIfContentIncomplete();
+        inOrder.verify(channelMethodProcessor).receiveTxSelect();
+        inOrder.verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @Test
+    void methodFrameIsNotDispatchedWhenContentSequenceIsIncomplete() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(channelMethodProcessor.rejectMethodFrameIfContentIncomplete()).thenReturn(true);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        final int bodySize = Integer.BYTES + Short.BYTES + 1 + 1 + Integer.BYTES;
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, bodySize);
+            buffer.putInt((QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID);
+            buffer.putUnsignedShort(0);
+            writeShortString(buffer, "");
+            buffer.put((byte) 0);
+            buffer.putUnsignedInt(0);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        final InOrder inOrder = inOrder(methodProcessor, channelMethodProcessor);
+        inOrder.verify(methodProcessor).setCurrentMethod(QueueDeclareBody.CLASS_ID, QueueDeclareBody.METHOD_ID);
+        inOrder.verify(methodProcessor).getChannelMethodProcessor(CHANNEL_ID);
+        inOrder.verify(channelMethodProcessor).rejectMethodFrameIfContentIncomplete();
+        inOrder.verify(methodProcessor).setCurrentMethod(0, 0);
+        verify(channelMethodProcessor, never()).receiveQueueDeclare(any(), anyBoolean(), anyBoolean(), anyBoolean(),
+                anyBoolean(), anyBoolean(), any());
+    }
+
+    @Test
+    void generatedChannelMethodWithTrailingBodyBytesIsRejectedBeforeChannelLookup() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(22))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 14);
+            buffer.putInt((BasicAckBody.CLASS_ID << 16) | BasicAckBody.METHOD_ID);
+            buffer.putLong(42L);
+            buffer.put((byte) 0);
+            buffer.put((byte) 1);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+    }
+
+    @Test
+    void directConnectionMethodWithTrailingBodyBytesIsRejectedBeforeDispatch() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(13))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, 5);
+            buffer.putInt(0x000a0033);
+            buffer.put((byte) 1);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        verify(methodProcessor, never()).receiveConnectionCloseOk();
+    }
+
+    @Test
+    void validUnknownMethodPublishesMetadataForCloseReporting() throws Exception
+    {
+        final int classId = 0x7FFF;
+        final int methodId = 1;
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+        final AMQFrameDecodingException exception;
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, Integer.BYTES);
+            buffer.putInt((classId << 16) | methodId);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+        }
+
+        assertEquals(ErrorCodes.COMMAND_INVALID, exception.getErrorCode());
+        assertEquals(classId, exception.getClassId());
+        assertEquals(methodId, exception.getMethodId());
+        final InOrder inOrder = inOrder(methodProcessor);
+        inOrder.verify(methodProcessor).setCurrentMethod(classId, methodId);
+        inOrder.verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @Test
+    void callbackFailureStillResetsCurrentMethod() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+        doThrow(new IllegalStateException("callback failed")).when(channelMethodProcessor).receiveTxSelect();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            assertThrows(IllegalStateException.class, () -> decoder.decodeBuffer(buffer));
+            assertFalse(buffer.hasRemaining());
+        }
+
+        verify(methodProcessor).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @ParameterizedTest
+    @MethodSource("decodingRuntimeFailures")
+    void runtimeFailureFromCallbackIsNotReportedAsMalformedWireData(final RuntimeException failure) throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+        doThrow(failure).when(channelMethodProcessor).receiveTxSelect();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            assertSame(failure, assertThrows(failure.getClass(), () -> decoder.decodeBuffer(buffer)));
+            assertFalse(buffer.hasRemaining());
+        }
+
+        verify(methodProcessor).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @Test
+    void runtimeFailureFromProtocolVersionLookupIsNotReportedAsMalformedWireData() throws Exception
+    {
+        final int classId = 0x7FFF;
+        final int methodId = 1;
+        final IllegalArgumentException failure = new IllegalArgumentException("Protocol version unavailable");
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenThrow(failure);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, Integer.BYTES);
+            buffer.putInt((classId << 16) | methodId);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            final ConnectionScopedRuntimeException exception = assertThrows(ConnectionScopedRuntimeException.class,
+                    () -> decoder.decodeBuffer(buffer));
+            assertSame(failure, exception.getCause());
+            assertFalse(buffer.hasRemaining());
+        }
+
+        verify(methodProcessor).setCurrentMethod(0, 0);
+        verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocolVersionsAndConnectionCloseOkMethods")
+    void validVersionSpecificConnectionCloseOkIsDispatched(final ProtocolVersion protocolVersion,
+                                                           final int classAndMethod) throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, 4);
+            buffer.putInt(classAndMethod);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        verify(methodProcessor).receiveConnectionCloseOk();
+        verify(methodProcessor).setCurrentMethod(classAndMethod >> 16, classAndMethod & 0xFFFF);
+        verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @Test
+    void validBasicPublishIsDispatchedWithDecodedArguments() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(21))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 13);
+            buffer.putInt((BasicPublishBody.CLASS_ID << 16) | BasicPublishBody.METHOD_ID);
+            buffer.putUnsignedShort(0);
+            writeShortString(buffer, "ex");
+            writeShortString(buffer, "rk");
+            buffer.put((byte) 1);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        verify(channelMethodProcessor).receiveBasicPublish(AMQShortString.valueOf("ex"),
+                AMQShortString.valueOf("rk"), true, false);
+        verify(channelMethodProcessor).rejectMethodFrameIfContentIncomplete();
+        verify(methodProcessor, times(1)).getChannelMethodProcessor(CHANNEL_ID);
+    }
+
+    @Test
+    void validConnectionMethodRetainsDecodedArgumentsUntilDispatch() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(18))
+        {
+            writeFrameHeader(buffer, (byte) 1, 0, 10);
+            buffer.putInt((ConnectionSecureOkBody.CLASS_ID << 16) | ConnectionSecureOkBody.METHOD_ID);
+            buffer.putUnsignedInt(2);
+            buffer.put((byte) 1);
+            buffer.put((byte) 2);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        final ArgumentCaptor<byte[]> response = ArgumentCaptor.forClass(byte[].class);
+        verify(methodProcessor).receiveConnectionSecureOk(response.capture());
+        assertArrayEquals(new byte[]{1, 2}, response.getValue());
+    }
+
+    @Test
+    void deferredMethodHonoursCloseOnlyMode() throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(channelMethodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        decoder.setExpectProtocolInitiation(false);
+
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 4);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.flip();
+
+            decoder.decodeBuffer(buffer);
+        }
+
+        verify(channelMethodProcessor, never()).receiveTxSelect();
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {2147483648L, 4294967295L})
+    void testDiscardPreservesUnsignedFrameSize(final long bodySize) throws Exception
+    {
+        final ServerDecoder decoder = new ServerDecoder(createMethodProcessor());
+        decoder.setExpectProtocolInitiation(false);
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(32))
+        {
+            writeFrameHeader(buffer, ContentBody.TYPE, CHANNEL_ID, bodySize);
+            buffer.flip();
+
+            assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+            assertFalse(buffer.hasRemaining());
+
+            buffer.clear();
+            buffer.put(new byte[buffer.remaining()]);
+            buffer.flip();
+            decoder.decodeBuffer(buffer);
+
+            assertFalse(buffer.hasRemaining(), "Discard must consume the fragment without retaining its payload");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testDiscardedFrameEndIsValidated(final boolean fragmented) throws Exception
+    {
+        final ServerDecoder decoder = new ServerDecoder(createMethodProcessor());
+        decoder.setExpectProtocolInitiation(false);
+        decoder.setMaxFrameSize(16);
+        final int bodySize = 17;
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+        {
+            writeFrameHeader(buffer, ContentBody.TYPE, CHANNEL_ID, bodySize);
+            buffer.put(new byte[bodySize]);
+            buffer.put((byte) 0);
+            buffer.flip();
+
+            assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+            if (fragmented)
+            {
+                buffer.limit(buffer.limit() - 1);
+                decoder.decodeBuffer(buffer);
+                assertFalse(buffer.hasRemaining());
+                buffer.limit(buffer.limit() + 1);
+            }
+            assertThrows(AMQFatalFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+            assertFalse(buffer.hasRemaining());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("decodingRuntimeFailures")
+    void testRuntimeDecodingFailureRetainsMethodContext(final RuntimeException failure) throws Exception
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        final QpidByteBuffer input = createFailingMethodBody(failure);
+
+        final AMQFrameDecodingException exception =
+                assertThrows(AMQFrameDecodingException.class, () -> decoder.processMethod(CHANNEL_ID, input));
+        assertSame(failure, exception.getCause());
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        assertEquals(BasicQosBody.CLASS_ID, exception.getClassId());
+        assertEquals(BasicQosBody.METHOD_ID, exception.getMethodId());
+        verify(methodProcessor).setCurrentMethod(0, 0);
+
+        decoder.setExpectProtocolInitiation(false);
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+        {
+            writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+            buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+            buffer.put(FRAME_END);
+            buffer.flip();
+            decoder.decodeBuffer(buffer);
+            assertFalse(buffer.hasRemaining());
+        }
+        verify(channelMethodProcessor).receiveTxSelect();
+    }
+
+    @ParameterizedTest
+    @MethodSource("scopedAndFatalFailures")
+    void testDecodingPreservesScopedAndFatalFailures(final Throwable failure)
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+        final QpidByteBuffer input = createFailingMethodBody(failure);
+
+        assertSame(failure, assertThrows(failure.getClass(), () -> decoder.processMethod(CHANNEL_ID, input)));
+        verify(methodProcessor).setCurrentMethod(0, 0);
+    }
+
+    @ParameterizedTest
+    @MethodSource("scopedAndFatalFailures")
+    void testProtocolVersionLookupPreservesScopedAndFatalFailures(final Throwable failure)
+    {
+        final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+        when(methodProcessor.getProtocolVersion()).thenThrow(failure);
+        final FrameBoundaryValidatingServerMethodProcessor validatingProcessor =
+                new FrameBoundaryValidatingServerMethodProcessor(methodProcessor);
+
+        assertSame(failure, assertThrows(failure.getClass(), validatingProcessor::getProtocolVersion));
+    }
+
+    private static QpidByteBuffer createFailingMethodBody(final Throwable failure)
+    {
+        final QpidByteBuffer input = mock(QpidByteBuffer.class);
+        when(input.remaining()).thenReturn(Integer.BYTES + Long.BYTES);
+        when(input.getInt()).thenReturn((BasicQosBody.CLASS_ID << 16) | BasicQosBody.METHOD_ID);
+        when(input.getUnsignedInt()).thenThrow(failure);
+        return input;
+    }
+
+    private static Stream<RuntimeException> decodingRuntimeFailures()
+    {
+        return Stream.of(new BufferUnderflowException(), new ArithmeticException("Injected decoding failure"),
+                new ClassCastException("Injected decoding failure"), new RuntimeException("Injected decoding failure"));
+    }
+
+    private static Stream<Throwable> scopedAndFatalFailures()
+    {
+        return Stream.of(new ConnectionScopedRuntimeException("Injected connection failure"),
+                new ServerScopedRuntimeException("Injected server failure"),
+                new StoreException("Injected store failure"), new InternalError("Injected JVM failure"));
+    }
+
+    private static ServerMethodProcessor<ServerChannelMethodProcessor> createMethodProcessor()
+    {
+        return mock(ServerMethodProcessor.class);
+    }
+
+    private static ServerDecoder createRejectingDecoder(
+            final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor)
+    {
+        return new ServerDecoder(methodProcessor)
+        {
+            private boolean _rejectFirstFrame = true;
+
+            @Override
+            protected void processFrame(final int channel,
+                                        final byte type,
+                                        final long bodySize,
+                                        final QpidByteBuffer body) throws AMQFrameDecodingException
+            {
+                if (_rejectFirstFrame)
+                {
+                    _rejectFirstFrame = false;
+                    body.getUnsignedShort();
+                    throw new AMQFrameDecodingException("Frame processing failed");
+                }
+                super.processFrame(channel, type, bodySize, body);
+            }
+        };
+    }
+
+    private static Stream<Arguments> rejectedFrameReadVariants()
+    {
+        return protocolVersionsAndConnectionCloseOkMethods()
+                .flatMap(arguments -> Stream.of(3, 7, 11, 12, 15, 23, 24)
+                        .flatMap(firstReadLength -> Stream.of(false, true)
+                                .map(direct -> Arguments.of(arguments.get()[0], arguments.get()[1],
+                                        firstReadLength, direct))));
+    }
+
+    private static Stream<Arguments> protocolVersionsAndConnectionCloseOkMethods()
+    {
+        return Stream.of(Arguments.of(ProtocolVersion.v0_8, 0x000a003d),
+                Arguments.of(ProtocolVersion.v0_9, 0x000a0033), Arguments.of(ProtocolVersion.v0_91, 0x000a0033));
+    }
+
+    private static Stream<Arguments> heartbeatChannelErrorCodes()
+    {
+        return Stream.of(Arguments.of(ProtocolVersion.v0_8, ErrorCodes.FRAME_ERROR),
+                Arguments.of(ProtocolVersion.v0_9, ErrorCodes.FRAME_ERROR),
+                Arguments.of(ProtocolVersion.v0_91, ErrorCodes.COMMAND_INVALID));
+    }
+
+    private static Stream<ProtocolVersion> legacyProtocolVersions()
+    {
+        return Stream.of(ProtocolVersion.v0_8, ProtocolVersion.v0_9);
+    }
+
+    private static void writeFrameHeader(final QpidByteBuffer buffer,
+                                         final byte type,
+                                         final int channel,
+                                         final long bodySize)
+    {
+        buffer.put(type);
+        buffer.putUnsignedShort(channel);
+        buffer.putUnsignedInt(bodySize);
+    }
+
+    private static void writeHeartbeatFrame(final QpidByteBuffer buffer,
+                                            final int channel,
+                                            final byte[] payload)
+    {
+        writeFrameHeader(buffer, HeartbeatBody.TYPE, channel, payload.length);
+        buffer.put(payload);
+        buffer.put(FRAME_END);
+    }
+
+    private static void writeShortString(final QpidByteBuffer buffer, final String value)
+    {
+        final byte[] encoded = value.getBytes(UTF_8);
+        buffer.put((byte) encoded.length);
+        buffer.put(encoded);
+    }
+
+    private static void writeConnectionStartOkFrame(final QpidByteBuffer buffer,
+                                                    final byte[] fieldTable,
+                                                    final int bodySize)
+    {
+        writeFrameHeader(buffer, (byte) 1, 0, bodySize);
+        buffer.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+        buffer.putUnsignedInt(fieldTable.length);
+        buffer.put(fieldTable);
+        writeShortString(buffer, "PLAIN");
+        buffer.putUnsignedInt(0);
+        writeShortString(buffer, "en_US");
+        buffer.put(FRAME_END);
+    }
+
+    private static byte[] nestedFieldTable(final int depth)
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+        for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+        {
+            buffer.put((byte) 1);
+            buffer.put((byte) 'n');
+            buffer.put(AMQType.FIELD_TABLE.identifier());
+            buffer.putInt(7 * (remainingDepth - 1));
+        }
+        buffer.put((byte) 1);
+        buffer.put((byte) 'v');
+        buffer.put(AMQType.INT.identifier());
+        buffer.putInt(42);
+        return buffer.array();
+    }
+
+    private static byte[] malformedNestedFieldTable()
+    {
+        final byte[] malformedNestedTable = ByteBuffer.allocate(7)
+                .put((byte) 1)
+                .put((byte) 's')
+                .put(AMQType.LONG_STRING.identifier())
+                .putInt(0x80000000)
+                .array();
+        return ByteBuffer.allocate(13 + malformedNestedTable.length)
+                .put((byte) 7)
+                .put("ignored".getBytes(UTF_8))
+                .put(AMQType.FIELD_TABLE.identifier())
+                .putInt(malformedNestedTable.length)
+                .put(malformedNestedTable)
+                .array();
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesDisposalTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesDisposalTest.java
new file mode 100644
index 0000000000..3658d5aaf5
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesDisposalTest.java
@@ -0,0 +1,460 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQType;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class BasicContentHeaderPropertiesDisposalTest extends UnitTestBase
+{
+    private static final int BUFFER_SIZE = 64;
+    private static final int CHANNEL_ID = 1;
+
+    @BeforeEach
+    void setUp()
+    {
+        QpidByteBuffer.deinitialisePool();
+        QpidByteBuffer.initialisePool(BUFFER_SIZE, 20, 1.0);
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        QpidByteBuffer.deinitialisePool();
+    }
+
+    @Test
+    void constructorDisposesEncodedFormWhenHeaderTableDecodeFails()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        in.putInt(0x20000000);   // headers field-table length
+        in.put((byte) 0);
+        in.flip();
+        final int size = in.remaining();
+
+        assertThrows(IllegalArgumentException.class, () -> new BasicContentHeaderProperties(in,
+                BasicContentHeaderProperties.HEADERS_MASK, size));
+
+        in.dispose();
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(), "Malformed content-header headers " +
+                "leaked a pooled direct buffer");
+    }
+
+    @Test
+    void constructorDisposesHeadersWhenLaterPropertyDecodeFails()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        in.putInt(3);                                  // headers field-table length
+        in.putUnsignedByte((short) 1);
+        in.put((byte) 'x');
+        in.put(AMQType.VOID.identifier());
+        in.putUnsignedByte((short) 1);                 // expiration string length
+        in.put((byte) 'x');                            // invalid numeric digit
+        in.flip();
+        final int size = in.remaining();
+
+        assertThrows(AMQFrameDecodingException.class, () -> new BasicContentHeaderProperties(in,
+                BasicContentHeaderProperties.HEADERS_MASK | BasicContentHeaderProperties.EXPIRATION_MASK, size));
+
+        in.dispose();
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "A later property failure leaked decoded content-header headers");
+    }
+
+    @Test
+    void nestedMalformedHeaderIsRejectedBeforeDispatch()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        final byte[] fieldTable = malformedNestedFieldTable();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+        in.putUnsignedShort(0);
+        in.putLong(0);
+        in.putUnsignedShort(BasicContentHeaderProperties.HEADERS_MASK);
+        in.putUnsignedInt(fieldTable.length);
+        in.put(fieldTable);
+        in.flip();
+        final int size = in.remaining();
+
+        final AMQFrameDecodingException exception = assertThrows(AMQFrameDecodingException.class, () ->
+                ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, size));
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+        verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+        in.dispose();
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Malformed nested content headers retained pooled direct memory");
+    }
+
+    @Test
+    void configuredNestingDepthRejectsContentHeaderBeforeDispatch()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        final byte[] fieldTable = nestedFieldTable(2);
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        try
+        {
+            in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+            in.putUnsignedShort(0);
+            in.putLong(0);
+            in.putUnsignedShort(BasicContentHeaderProperties.HEADERS_MASK);
+            in.putUnsignedInt(fieldTable.length);
+            in.put(fieldTable);
+            in.flip();
+            final int size = in.remaining();
+
+            final AMQFrameDecodingException exception = assertThrows(AMQFrameDecodingException.class, () ->
+                    ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, size, 1));
+
+            assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+            verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+        }
+        finally
+        {
+            in.dispose();
+        }
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Content headers rejected by configured nesting depth retained pooled direct memory");
+    }
+
+    @Test
+    void propertyPaddingIsRejectedBeforeDispatch()
+    {
+        assertContentHeaderRejectedBeforeDispatch(0, 0, 0, new byte[] { (byte) 0xFF }, null);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0x0001, 0x0002 })
+    void unsupportedPropertyFlagsAreRejectedBeforeDispatch(final int propertyFlags)
+    {
+        assertContentHeaderRejectedBeforeDispatch(0, 0, propertyFlags, new byte[0], null);
+    }
+
+    @Test
+    void unsignedBodySizeOutsideSignedRangeIsRejectedWithoutDecodingProperties() throws AMQFrameDecodingException
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        try
+        {
+            in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+            in.putUnsignedShort(0);
+            in.putLong(Long.MIN_VALUE);
+            in.putUnsignedShort(0);
+            in.put((byte) 0xFF);
+            in.flip();
+
+            ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining());
+
+            assertFalse(in.hasRemaining());
+            verify(methodProcessor).receiveOversizedMessageHeader(CHANNEL_ID, Long.MIN_VALUE);
+            verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+            verify(methodProcessor, never()).getProtocolVersion();
+        }
+        finally
+        {
+            in.dispose();
+        }
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Oversized content header retained pooled direct memory");
+    }
+
+    @Test
+    void amqp08StructuredContentIsRejectedAsNotImplementedBeforeDispatch()
+    {
+        final AMQFrameDecodingException exception =
+                assertContentHeaderRejectedBeforeDispatch(1, 0, 0, new byte[0], ProtocolVersion.v0_8);
+
+        assertEquals(ErrorCodes.NOT_IMPLEMENTED, exception.getErrorCode());
+    }
+
+    @Test
+    void amqp09ReservedContentWeightIsRejectedAsFrameErrorBeforeDispatch()
+    {
+        assertReservedContentWeightRejected(ProtocolVersion.v0_9);
+    }
+
+    @Test
+    void amqp091NonZeroContentWeightIsRejectedAsFrameErrorBeforeDispatch()
+    {
+        assertReservedContentWeightRejected(ProtocolVersion.v0_91);
+    }
+
+    @Test
+    void zeroContentWeightDispatchesWithoutProtocolVersionLookup() throws AMQFrameDecodingException
+    {
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        final ChannelMethodProcessor channelMethodProcessor = mock(ChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+        in.putUnsignedShort(0);
+        in.putLong(0);
+        in.putUnsignedShort(0);
+        in.flip();
+
+        final ArgumentCaptor<BasicContentHeaderProperties> propertiesCaptor =
+                ArgumentCaptor.forClass(BasicContentHeaderProperties.class);
+        try
+        {
+            ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining());
+
+            verify(methodProcessor, never()).getProtocolVersion();
+            verify(channelMethodProcessor).receiveMessageHeader(propertiesCaptor.capture(), eq(0L));
+        }
+        finally
+        {
+            if (!propertiesCaptor.getAllValues().isEmpty())
+            {
+                propertiesCaptor.getValue().dispose();
+            }
+            in.dispose();
+        }
+    }
+
+    @Test
+    void channelLookupFailureDisposesContentHeaderProperties()
+    {
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID))
+                .thenThrow(new IllegalStateException("Connection is not open"));
+
+        assertProcessorFailureDoesNotRetainContentHeaderProperties(methodProcessor);
+    }
+
+    @Test
+    void channelDispatchFailurePreservesTransferredContentHeaderProperties()
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        final ChannelMethodProcessor channelMethodProcessor = mock(ChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+        final AtomicReference<BasicContentHeaderProperties> retainedProperties = new AtomicReference<>();
+        final IllegalStateException failure = new IllegalStateException("Dispatch failed after retaining properties");
+        doAnswer(invocation ->
+        {
+            retainedProperties.set(invocation.getArgument(0));
+            throw failure;
+        })
+                .when(channelMethodProcessor)
+                .receiveMessageHeader(any(BasicContentHeaderProperties.class), anyLong());
+
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        try
+        {
+            in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+            in.putUnsignedShort(0);
+            in.putLong(0);
+            in.putUnsignedShort(BasicContentHeaderProperties.HEADERS_MASK);
+            in.putUnsignedInt(7);
+            in.putUnsignedByte((short) 1);
+            in.put((byte) 'v');
+            in.put(AMQType.INT.identifier());
+            in.putInt(42);
+            in.flip();
+
+            final IllegalStateException thrown = assertThrows(IllegalStateException.class, () ->
+                    ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining()));
+            assertSame(failure, thrown);
+        }
+        finally
+        {
+            in.dispose();
+        }
+
+        final BasicContentHeaderProperties properties = retainedProperties.get();
+        assertNotNull(properties, "Content-header properties were not retained by the receiver");
+        try
+        {
+            assertEquals(42, properties.getHeader("v"), "Transferred field-table buffer was released by the parser");
+        }
+        finally
+        {
+            properties.dispose();
+        }
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Transferred content-header properties retained pooled direct memory after disposal");
+    }
+
+    @Test
+    void channelDispatchFailureDoesNotLeakPropertiesDisposedByReceiver()
+    {
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        final ChannelMethodProcessor channelMethodProcessor = mock(ChannelMethodProcessor.class);
+        when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+        doAnswer(invocation ->
+        {
+            final BasicContentHeaderProperties properties = invocation.getArgument(0);
+            properties.dispose();
+            throw new IllegalStateException("Dispatch rejected properties");
+        })
+                .when(channelMethodProcessor)
+                .receiveMessageHeader(any(BasicContentHeaderProperties.class), anyLong());
+
+        assertProcessorFailureDoesNotRetainContentHeaderProperties(methodProcessor);
+    }
+
+    private void assertProcessorFailureDoesNotRetainContentHeaderProperties(
+            final MethodProcessor<ChannelMethodProcessor> methodProcessor)
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        try
+        {
+            in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+            in.putUnsignedShort(0);
+            in.putLong(0);
+            in.putUnsignedShort(0);
+            in.flip();
+
+            assertThrows(IllegalStateException.class, () ->
+                    ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining()));
+        }
+        finally
+        {
+            in.dispose();
+        }
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Processor failure retained content-header properties");
+    }
+
+    private void assertReservedContentWeightRejected(final ProtocolVersion protocolVersion)
+    {
+        final AMQFrameDecodingException exception =
+                assertContentHeaderRejectedBeforeDispatch(1, 0, 0, new byte[0], protocolVersion);
+
+        assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+    }
+
+    private AMQFrameDecodingException assertContentHeaderRejectedBeforeDispatch(final int weight,
+                                                                                final long bodySize,
+                                                                                final int propertyFlags,
+                                                                                final byte[] propertyList,
+                                                                                final ProtocolVersion protocolVersion)
+    {
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+        if (protocolVersion != null)
+        {
+            when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+        }
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        final AMQFrameDecodingException exception;
+        try
+        {
+            in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+            in.putUnsignedShort(weight);
+            in.putLong(bodySize);
+            in.putUnsignedShort(propertyFlags);
+            in.put(propertyList);
+            in.flip();
+            final int size = in.remaining();
+
+            exception = assertThrows(AMQFrameDecodingException.class, () ->
+                    ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, size));
+
+            verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+        }
+        finally
+        {
+            in.dispose();
+        }
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Rejected content-header properties retained pooled direct memory");
+        return exception;
+    }
+
+    private static MethodProcessor<ChannelMethodProcessor> mockMethodProcessor()
+    {
+        return mock(MethodProcessor.class);
+    }
+
+    private static byte[] malformedNestedFieldTable()
+    {
+        final byte[] malformedNestedTable = ByteBuffer.allocate(7)
+                .put((byte) 1)
+                .put((byte) 's')
+                .put(AMQType.LONG_STRING.identifier())
+                .putInt(0x80000000)
+                .array();
+        return ByteBuffer.allocate(7 + malformedNestedTable.length)
+                .put((byte) 1)
+                .put((byte) 'n')
+                .put(AMQType.FIELD_TABLE.identifier())
+                .putInt(malformedNestedTable.length)
+                .put(malformedNestedTable)
+                .array();
+    }
+
+    private static byte[] nestedFieldTable(final int depth)
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+        for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+        {
+            buffer.put((byte) 1);
+            buffer.put((byte) 'n');
+            buffer.put(AMQType.FIELD_TABLE.identifier());
+            buffer.putInt(7 * (remainingDepth - 1));
+        }
+        buffer.put((byte) 1);
+        buffer.put((byte) 'v');
+        buffer.put(AMQType.INT.identifier());
+        buffer.putInt(42);
+        return buffer.array();
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java
index 47d0a59762..6043b3dfaa 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java
@@ -91,11 +91,15 @@ class BasicContentHeaderPropertiesTest extends UnitTestBase
     }
 
     @Test
-    void populatePropertiesFromBuffer() throws Exception
+    void populateEmptyPropertiesFromBuffer() throws Exception
     {
-        final QpidByteBuffer buf = QpidByteBuffer.wrap(new byte[300]);
-        _testProperties.dispose();
-        _testProperties = new BasicContentHeaderProperties(buf, 99, 99);
+        try (final QpidByteBuffer buf = QpidByteBuffer.wrap(new byte[0]))
+        {
+            _testProperties.dispose();
+            _testProperties = new BasicContentHeaderProperties(buf, 0, 0);
+            assertEquals(0, _testProperties.getPropertyFlags());
+            _testProperties.dispose();
+        }
     }
 
     @Test
@@ -238,16 +242,23 @@ class BasicContentHeaderPropertiesTest extends UnitTestBase
                     propertiesBuffer.flip();
 
                     final BasicContentHeaderProperties testProperties = new BasicContentHeaderProperties(propertiesBuffer, flags, propertyListSize);
-                    final Map<String, Object> headersBeforeReallocation = testProperties.getHeadersAsMap();
-                    assertEquals(headers, headersBeforeReallocation, "Unexpected headers");
-
-                    buffer.dispose();
-
-                    assertTrue(propertiesBuffer.isSparse(), "Properties buffer should be sparse");
-                    testProperties.reallocate();
-
-                    final Map<String, Object> headersAfterReallocation = testProperties.getHeadersAsMap();
-                    assertEquals(headers, headersAfterReallocation, "Unexpected headers after re-allocation");
+                    try
+                    {
+                        final Map<String, Object> headersBeforeReallocation = testProperties.getHeadersAsMap();
+                        assertEquals(headers, headersBeforeReallocation, "Unexpected headers");
+
+                        buffer.dispose();
+
+                        assertTrue(propertiesBuffer.isSparse(), "Properties buffer should be sparse");
+                        testProperties.reallocate();
+
+                        final Map<String, Object> headersAfterReallocation = testProperties.getHeadersAsMap();
+                        assertEquals(headers, headersAfterReallocation, "Unexpected headers after re-allocation");
+                    }
+                    finally
+                    {
+                        testProperties.dispose();
+                    }
                 }
             }
         }
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ClientFieldTableBodyDisposalTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ClientFieldTableBodyDisposalTest.java
new file mode 100644
index 0000000000..25cb803d0d
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ClientFieldTableBodyDisposalTest.java
@@ -0,0 +1,106 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.v0_8.AMQShortString;
+import org.apache.qpid.server.protocol.v0_8.AMQType;
+import org.apache.qpid.server.protocol.v0_8.FieldTable;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ClientFieldTableBodyDisposalTest extends UnitTestBase
+{
+    private static final int BUFFER_SIZE = 64;
+
+    @BeforeEach
+    void setUp()
+    {
+        QpidByteBuffer.deinitialisePool();
+        QpidByteBuffer.initialisePool(BUFFER_SIZE, 20, 1.0);
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        QpidByteBuffer.deinitialisePool();
+    }
+
+    @Test
+    void connectionStartDisposesServerPropertiesWhenLaterFieldDecodeFails()
+    {
+        final ClientMethodProcessor<ClientChannelMethodProcessor> dispatcher = mock(ClientMethodProcessor.class);
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        in.putUnsignedByte((short) 0);               // version major
+        in.putUnsignedByte((short) 9);               // version minor
+        writeValidFieldTable(in);
+        in.putInt(0x20000000);                       // mechanisms length
+        in.flip();
+
+        assertThrows(IllegalArgumentException.class, () -> ConnectionStartBody.process(in, dispatcher));
+
+        in.dispose();
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Malformed connection.start leaked server properties");
+    }
+
+    @Test
+    void channelAlertDisposesDetailsWhenProcessorFails()
+    {
+        final ClientChannelMethodProcessor dispatcher = mock(ClientChannelMethodProcessor.class);
+        doThrow(new IllegalStateException("processor failure"))
+                .when(dispatcher)
+                .receiveChannelAlert(anyInt(), any(AMQShortString.class), any(FieldTable.class));
+
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        in.putUnsignedShort(500);
+        in.putUnsignedByte((short) 1);
+        in.put((byte) 'x');
+        writeValidFieldTable(in);
+        in.flip();
+
+        assertThrows(IllegalStateException.class, () -> ChannelAlertBody.process(in, dispatcher));
+
+        in.dispose();
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "A channel.alert processor failure leaked details");
+    }
+
+    private static void writeValidFieldTable(final QpidByteBuffer buffer)
+    {
+        buffer.putInt(3);
+        buffer.putUnsignedByte((short) 1);
+        buffer.put((byte) 'x');
+        buffer.put(AMQType.VOID.identifier());
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBodyTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBodyTest.java
new file mode 100644
index 0000000000..94bd40f1c5
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBodyTest.java
@@ -0,0 +1,126 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ConnectionStartOkBodyTest extends UnitTestBase
+{
+    private static final int BUFFER_SIZE = 64;
+    private static final int POOL_SIZE = 20;
+    private static final double SPARSITY_FRACTION = 1.0;
+
+    @BeforeEach
+    void setUp()
+    {
+        QpidByteBuffer.deinitialisePool();
+        QpidByteBuffer.initialisePool(BUFFER_SIZE, POOL_SIZE, SPARSITY_FRACTION);
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        QpidByteBuffer.deinitialisePool();
+    }
+
+    @Test
+    void processDisposesClientPropertiesWhenFieldTableDecodeFails()
+    {
+        final ServerMethodProcessor dispatcher = mock(ServerMethodProcessor.class);
+
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        // client-properties field table (length 7): entry key "s", value type 'S', long-string length 0x80000000
+        in.putInt(7);
+        in.putUnsignedByte((short) 1);
+        in.put((byte) 's');
+        in.put((byte) 'S');
+        in.putInt(0x80000000);
+        // mechanism "PLAIN" (short string)
+        in.putUnsignedByte((short) 5);
+        in.put("PLAIN".getBytes(StandardCharsets.US_ASCII));
+        // response "guest" (long bytes)
+        in.putInt(5);
+        in.put("guest".getBytes(StandardCharsets.US_ASCII));
+        // locale "en_US" (short string)
+        in.putUnsignedByte((short) 5);
+        in.put("en_US".getBytes(StandardCharsets.US_ASCII));
+        in.flip();
+
+        assertThrows(IllegalArgumentException.class, () -> ConnectionStartOkBody.process(in, dispatcher));
+
+        in.dispose();
+
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Malformed start-ok leaked a pooled direct buffer");
+    }
+
+    @Test
+    void processDisposesNestedClientPropertyBeforeLaterDecodeFailure()
+    {
+        final ServerMethodProcessor dispatcher = mock(ServerMethodProcessor.class);
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+
+        // client-properties field table: a valid nested table followed by an invalid long-string length
+        in.putInt(17);
+        in.putUnsignedByte((short) 1);
+        in.put((byte) 'n');
+        in.put((byte) 'F');
+        in.putInt(3);
+        in.putUnsignedByte((short) 1);
+        in.put((byte) 'x');
+        in.put((byte) 'V');
+        in.putUnsignedByte((short) 1);
+        in.put((byte) 's');
+        in.put((byte) 'S');
+        in.putInt(0x80000000);
+        // mechanism "PLAIN" (short string)
+        in.putUnsignedByte((short) 5);
+        in.put("PLAIN".getBytes(StandardCharsets.US_ASCII));
+        // response "guest" (long bytes)
+        in.putInt(5);
+        in.put("guest".getBytes(StandardCharsets.US_ASCII));
+        // locale "en_US" (short string)
+        in.putUnsignedByte((short) 5);
+        in.put("en_US".getBytes(StandardCharsets.US_ASCII));
+        in.flip();
+
+        assertThrows(IllegalArgumentException.class, () -> ConnectionStartOkBody.process(in, dispatcher));
+
+        in.dispose();
+
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Malformed start-ok leaked a nested client-properties buffer");
+    }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ServerFieldTableBodyDisposalTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ServerFieldTableBodyDisposalTest.java
new file mode 100644
index 0000000000..2a9a32b5f2
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ServerFieldTableBodyDisposalTest.java
@@ -0,0 +1,160 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ServerFieldTableBodyDisposalTest extends UnitTestBase
+{
+    private static final int BUFFER_SIZE = 64;
+
+    @FunctionalInterface
+    private interface Decoder
+    {
+        void process(final QpidByteBuffer buffer, final ServerChannelMethodProcessor dispatcher) throws Exception;
+    }
+
+    @FunctionalInterface
+    private interface ArgWriter
+    {
+        void write(final QpidByteBuffer buffer);
+    }
+
+    @BeforeEach
+    void setUp()
+    {
+        QpidByteBuffer.deinitialisePool();
+        QpidByteBuffer.initialisePool(BUFFER_SIZE, 20, 1.0);
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        QpidByteBuffer.deinitialisePool();
+    }
+
+    @Test
+    void basicConsumeDisposesArgumentsOnMalformedTable()
+    {
+        assertNoLeak(buffer ->
+        {
+            buffer.putShort((short) 0);           // ticket
+            writeShortString(buffer, "q");        // queue
+            writeShortString(buffer, "tag");      // consumerTag
+            buffer.put((byte) 0);                 // bitfield
+            writeMalformedFieldTable(buffer);
+        }, BasicConsumeBody::process);
+    }
+
+    @Test
+    void exchangeDeclareDisposesArgumentsOnMalformedTable()
+    {
+        assertNoLeak(buffer ->
+        {
+            buffer.putShort((short) 0);           // ticket
+            writeShortString(buffer, "ex");       // exchange
+            writeShortString(buffer, "direct");   // type
+            buffer.put((byte) 0);                 // bitfield
+            writeMalformedFieldTable(buffer);
+        }, ExchangeDeclareBody::process);
+    }
+
+    @Test
+    void queueDeclareDisposesArgumentsOnMalformedTable()
+    {
+        assertNoLeak(buffer ->
+        {
+            buffer.putShort((short) 0);           // ticket
+            writeShortString(buffer, "q");        // queue
+            buffer.put((byte) 0);                 // bitfield
+            writeMalformedFieldTable(buffer);
+        }, QueueDeclareBody::process);
+    }
+
+    @Test
+    void queueBindDisposesArgumentsOnMalformedTable()
+    {
+        assertNoLeak(buffer ->
+        {
+            buffer.putShort((short) 0);           // ticket
+            writeShortString(buffer, "q");        // queue
+            writeShortString(buffer, "ex");       // exchange
+            writeShortString(buffer, "rk");       // bindingKey
+            buffer.put((byte) 0);                 // nowait
+            writeMalformedFieldTable(buffer);
+        }, QueueBindBody::process);
+    }
+
+    @Test
+    void queueUnbindDisposesArgumentsOnMalformedTable()
+    {
+        assertNoLeak(buffer ->
+        {
+            buffer.putShort((short) 0);           // ticket
+            writeShortString(buffer, "q");        // queue
+            writeShortString(buffer, "ex");       // exchange
+            writeShortString(buffer, "rk");       // routingKey
+            writeMalformedFieldTable(buffer);
+        }, QueueUnbindBody::process);
+    }
+
+    private void assertNoLeak(final ArgWriter argWriter, final Decoder decoder)
+    {
+        final ServerChannelMethodProcessor dispatcher = mock(ServerChannelMethodProcessor.class);
+        final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+        final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+        argWriter.write(in);
+        in.flip();
+
+        assertThrows(IllegalArgumentException.class, () -> decoder.process(in, dispatcher));
+
+        in.dispose();
+        assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+                "Malformed field-table argument leaked a pooled direct buffer");
+    }
+
+    private static void writeShortString(final QpidByteBuffer buffer, final String value)
+    {
+        final byte[] bytes = value.getBytes(US_ASCII);
+        buffer.putUnsignedByte((short) bytes.length);
+        buffer.put(bytes);
+    }
+
+    // field table (length 7): entry key "s", value type 'S', long-string length 0x80000000
+    private static void writeMalformedFieldTable(final QpidByteBuffer buffer)
+    {
+        buffer.putInt(7);
+        buffer.putUnsignedByte((short) 1);
+        buffer.put((byte) 's');
+        buffer.put((byte) 'S');
+        buffer.putInt(0x80000000);
+    }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java
index bdc68b1f23..f213ee67f3 100644
--- a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java
@@ -33,14 +33,18 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
-import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
-import java.util.concurrent.ExecutionException;
 
 import org.junit.jupiter.api.Test;
 
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.AMQDecoder;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseOkBody;
@@ -49,6 +53,7 @@ import org.apache.qpid.server.protocol.v0_8.transport.ConnectionSecureBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneBody;
 import org.apache.qpid.server.protocol.v0_8.transport.HeartbeatBody;
+import org.apache.qpid.server.transport.ByteBufferSender;
 import org.apache.qpid.tests.protocol.ChannelClosedResponse;
 import org.apache.qpid.tests.protocol.Response;
 import org.apache.qpid.tests.protocol.SpecificationTest;
@@ -60,6 +65,9 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
     private static final String ANONYMOUS = "ANONYMOUS";
     private static final String PLAIN = "PLAIN";
     private static final String CRAM_MD5 = "CRAM-MD5";
+    private static final byte FRAME_END = (byte) 0xCE;
+    private static final int CONNECTION_CLOSE_METHOD_ID_0_8 = 60;
+    private static final int CONNECTION_CLOSE_METHOD_ID_0_9 = 50;
 
     @Test
     @SpecificationTest(section = "1.4.2.1", description = "start connection negotiation")
@@ -104,10 +112,8 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
                        .connection().startOkMechanism("NOT-A-MECHANISM")
                                     .startOk();
 
-            final ConnectionCloseBody res = interaction.consumeResponse()
-                                                       .getLatestResponse(ConnectionCloseBody.class);
-            assertThat(res.getReplyCode(), is(equalTo(ErrorCodes.CONNECTION_FORCED)));
-
+            interaction.sync();
+            transport.assertNoMoreResponsesAndChannelClosed();
         }
     }
 
@@ -301,39 +307,64 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
             final int overlyLargeFrameBodySize = (int) (frameMax + 1);  // Should be frameMax - 8 + 1.
             final byte[] bodyBytes = new byte[overlyLargeFrameBodySize];
 
-            interaction.connection()
-                       .tuneOkChannelMax(response.getChannelMax())
-                       .tuneOkFrameMax(frameMax)
-                       .tuneOkHeartbeat(response.getHeartbeat())
-                       .tuneOk()
-                       .connection().open()
-                       .consumeResponse(ConnectionOpenOkBody.class)
-                       .channel().open()
-                       .consumeResponse(ChannelOpenOkBody.class)
-                       .basic().publish()
-                       .basic().contentHeader(bodyBytes.length)
-                       .basic().contentBody(bodyBytes);
+            final ConnectionCloseBody close = interaction.connection()
+                    .tuneOkChannelMax(response.getChannelMax())
+                    .tuneOkFrameMax(frameMax)
+                    .tuneOkHeartbeat(response.getHeartbeat())
+                    .tuneOk()
+                    .connection().open()
+                    .consumeResponse(ConnectionOpenOkBody.class)
+                    .channel().open()
+                    .consumeResponse(ChannelOpenOkBody.class)
+                    .basic().publish()
+                    .basic().contentHeader(bodyBytes.length)
+                    .basic().contentBody(bodyBytes)
+                    .consumeResponse()
+                    .getLatestResponse(ConnectionCloseBody.class);
+
+            assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.FRAME_ERROR)));
+        }
+    }
 
-            // Spec requires:
-            //assertThat(res.getReplyCode(), CoreMatchers.is(CoreMatchers.equalTo(ErrorCodes.COMMAND_INVALID)));
+    @Test
+    @SpecificationTest(section = "4.2.3",
+            description = "A Connection-class method on a non-zero channel must produce reply code 503.")
+    public void connectionMethodOnNonZeroChannelSignalsConnectionException() throws Exception
+    {
+        try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+        {
+            final ConnectionCloseBody close = transport.newInteraction()
+                    .negotiateOpen()
+                    .channelId(1)
+                    .connection().close()
+                    .consumeResponse()
+                    .getLatestResponse(ConnectionCloseBody.class);
+
+            assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.COMMAND_INVALID)));
+            assertThat(close.getClassId(), is(equalTo(ConnectionCloseBody.CLASS_ID)));
+            final int expectedMethodId = ProtocolVersion.v0_8.equals(transport.getProtocolVersion())
+                    ? CONNECTION_CLOSE_METHOD_ID_0_8
+                    : CONNECTION_CLOSE_METHOD_ID_0_9;
+            assertThat(close.getMethodId(), is(equalTo(expectedMethodId)));
+        }
+    }
 
-            // Server actually abruptly closes the connection.  We might see a graceful TCP/IP close or a broken pipe.
-            try
-            {
-                interaction.consumeResponse().getLatestResponse(ChannelClosedResponse.class);
-            }
-            catch (ExecutionException e)
-            {
-                Throwable original = e.getCause();
-                if (original instanceof IOException)
-                {
-                    // PASS
-                }
-                else
-                {
-                    throw new RuntimeException(original);
-                }
-            }
+    @Test
+    @SpecificationTest(section = "4.2.3",
+            description = "A malformed method frame must produce reply code 501 and identify the failing method.")
+    public void truncatedMethodSignalsConnectionExceptionWithMethodIdentifiers() throws Exception
+    {
+        try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+        {
+            final ConnectionCloseBody close = transport.newInteraction()
+                    .negotiateOpen()
+                    .sendPerformative(truncatedBasicQosFrame())
+                    .consumeResponse()
+                    .getLatestResponse(ConnectionCloseBody.class);
+
+            assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.FRAME_ERROR)));
+            assertThat(close.getClassId(), is(equalTo(BasicQosBody.CLASS_ID)));
+            assertThat(close.getMethodId(), is(equalTo(BasicQosBody.METHOD_ID)));
         }
     }
 
@@ -517,4 +548,34 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
             assertThat(String.valueOf(close.getReplyText()).toLowerCase(), containsString("unknown virtual host"));
         }
     }
+
+    private static AMQDataBlock truncatedBasicQosFrame()
+    {
+        final ByteBuffer buffer = ByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + Integer.BYTES + 1);
+        buffer.put((byte) 1);
+        buffer.putShort((short) 1);
+        buffer.putInt(Integer.BYTES);
+        buffer.putInt((BasicQosBody.CLASS_ID << 16) | BasicQosBody.METHOD_ID);
+        buffer.put(FRAME_END);
+        return rawFrame(buffer.array());
+    }
+
+    private static AMQDataBlock rawFrame(final byte[] bytes)
+    {
+        return new AMQDataBlock()
+        {
+            @Override
+            public long getSize()
+            {
+                return bytes.length;
+            }
+
+            @Override
+            public long writePayload(final ByteBufferSender sender)
+            {
+                sender.send(QpidByteBuffer.wrap(bytes));
+                return bytes.length;
+            }
+        };
+    }
 }
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ContentBodyFrameLimitTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ContentBodyFrameLimitTest.java
new file mode 100644
index 0000000000..581e3dd874
--- /dev/null
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ContentBodyFrameLimitTest.java
@@ -0,0 +1,108 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.tests.protocol.v0_8;
+
+import static org.apache.qpid.tests.utils.BrokerAdmin.KIND_BROKER_J;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelCloseOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.tests.utils.BrokerAdmin;
+import org.apache.qpid.tests.utils.BrokerAdminUsingTestBase;
+import org.apache.qpid.tests.utils.BrokerSpecific;
+import org.apache.qpid.tests.utils.ConfigItem;
+
+@BrokerSpecific(kind = KIND_BROKER_J)
+@ConfigItem(name = "connection.maxContentBodyFramesPerMessage", value = "2")
+public class ContentBodyFrameLimitTest extends BrokerAdminUsingTestBase
+{
+    private static final List<Protocol> PROTOCOLS = List.of(Protocol.AMQP_0_8, Protocol.AMQP_0_9, Protocol.AMQP_0_9_1);
+
+    @BeforeEach
+    public void setUp()
+    {
+        getBrokerAdmin().createQueue(BrokerAdmin.TEST_QUEUE_NAME);
+    }
+
+    @Test
+    public void contentBodyFrameLimitExceededClosesConnection() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport);
+                interaction.basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentHeader(1)
+                        .basic().contentBody(new byte[0])
+                        .basic().contentBody(new byte[0])
+                        .basic().contentBody(new byte[0]);
+
+                final ConnectionCloseBody close = interaction.consumeResponse()
+                        .getLatestResponse(ConnectionCloseBody.class);
+                assertThat(protocol.toString(), close.getReplyCode(), is(equalTo(ErrorCodes.RESOURCE_ERROR)));
+            }
+        }
+    }
+
+    @Test
+    public void messageAtExactContentBodyFrameLimitIsAccepted() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport);
+                interaction.basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentHeader(1)
+                        .basic().contentBody(new byte[0])
+                        .basic().contentBody(new byte[]{42})
+                        .channel().close()
+                        .consumeResponse(ChannelCloseOkBody.class);
+            }
+        }
+
+        assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(PROTOCOLS.size())));
+    }
+
+    private Interaction openChannel(final FrameTransport transport) throws Exception
+    {
+        return transport.newInteraction()
+                .negotiateOpen()
+                .channel().open().consumeResponse(ChannelOpenOkBody.class);
+    }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/IncompleteContentSequenceTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/IncompleteContentSequenceTest.java
new file mode 100644
index 0000000000..ab2c0cbf42
--- /dev/null
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/IncompleteContentSequenceTest.java
@@ -0,0 +1,186 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.tests.protocol.v0_8;
+
+import static org.apache.qpid.tests.utils.BrokerAdmin.KIND_BROKER_J;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareOkBody;
+import org.apache.qpid.tests.utils.BrokerAdmin;
+import org.apache.qpid.tests.utils.BrokerAdminUsingTestBase;
+import org.apache.qpid.tests.utils.BrokerSpecific;
+
+@BrokerSpecific(kind = KIND_BROKER_J)
+public class IncompleteContentSequenceTest extends BrokerAdminUsingTestBase
+{
+    private static final List<Protocol> PROTOCOLS = List.of(Protocol.AMQP_0_8, Protocol.AMQP_0_9, Protocol.AMQP_0_9_1);
+
+    @BeforeEach
+    public void setUp()
+    {
+        getBrokerAdmin().createQueue(BrokerAdmin.TEST_QUEUE_NAME);
+    }
+
+    @Test
+    public void secondPublishBeforeContentCompletionClosesConnection() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport);
+                interaction.basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentHeader(2)
+                        .basic().contentBody(new byte[]{42})
+                        .basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish();
+
+                assertIncompleteContentClose(interaction, protocol);
+            }
+        }
+    }
+
+    @Test
+    public void methodBeforeContentCompletionClosesConnection() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport);
+                interaction.basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentHeader(2)
+                        .basic().contentBody(new byte[]{42})
+                        .queue().declarePassive(true)
+                        .declareName(BrokerAdmin.TEST_QUEUE_NAME)
+                        .declare();
+
+                assertIncompleteContentClose(interaction, protocol);
+            }
+        }
+    }
+
+    @Test
+    public void methodOnDifferentChannelIsAllowedDuringContentSequence() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport)
+                        .channelId(2).channel().open().consumeResponse(ChannelOpenOkBody.class);
+                interaction.channelId(1)
+                        .basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentHeader(2)
+                        .basic().contentBody(new byte[]{42});
+
+                interaction.channelId(2)
+                        .queue().declarePassive(true)
+                        .declareName(BrokerAdmin.TEST_QUEUE_NAME)
+                        .declare()
+                        .consumeResponse(QueueDeclareOkBody.class);
+
+                interaction.channelId(1).basic().contentBody(new byte[]{43})
+                        .channelId(0).connection().close()
+                        .consumeResponse(ConnectionCloseOkBody.class);
+            }
+        }
+    }
+
+    @Test
+    public void duplicateContentHeaderClosesConnection() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport);
+                interaction.basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentHeader(1)
+                        .basic().contentHeader(1);
+
+                assertIncompleteContentClose(interaction, protocol);
+            }
+        }
+    }
+
+    @Test
+    public void contentBodyBeforeHeaderClosesConnection() throws Exception
+    {
+        for (final Protocol protocol : PROTOCOLS)
+        {
+            try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+                    .getPreferredPortType(), protocol).connect())
+            {
+                final Interaction interaction = openChannel(transport);
+                interaction.basic().publishExchange("")
+                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+                        .publish()
+                        .basic().contentBody(new byte[0]);
+
+                assertIncompleteContentClose(interaction, protocol);
+            }
+        }
+    }
+
+    private void assertIncompleteContentClose(final Interaction interaction, final Protocol protocol)
+            throws Exception
+    {
+        final ConnectionCloseBody close = interaction.consumeResponse().getLatestResponse(ConnectionCloseBody.class);
+        final int expectedReplyCode = protocol == Protocol.AMQP_0_9_1
+                ? ErrorCodes.UNEXPECTED_FRAME
+                : ErrorCodes.FRAME_ERROR;
+        assertThat(protocol.toString(), close.getReplyCode(), is(equalTo(expectedReplyCode)));
+    }
+
+    private Interaction openChannel(final FrameTransport transport) throws Exception
+    {
+        return transport.newInteraction()
+                .negotiateOpen()
+                .channelId(1)
+                .channel().open().consumeResponse(ChannelOpenOkBody.class);
+    }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/MalformedFrameTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/MalformedFrameTest.java
new file mode 100644
index 0000000000..8204ed93e0
--- /dev/null
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/MalformedFrameTest.java
@@ -0,0 +1,244 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.tests.protocol.v0_8;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartBody;
+import org.apache.qpid.server.transport.ByteBufferSender;
+import org.apache.qpid.tests.protocol.ChannelClosedResponse;
+import org.apache.qpid.tests.utils.BrokerAdminUsingTestBase;
+
+public class MalformedFrameTest extends BrokerAdminUsingTestBase
+{
+    private static final byte FRAME_END = (byte) 0xCE;
+    private static final int MAX_NESTED_OBJECTS = Integer.getInteger(AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS,
+            AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+
+    @Test
+    public void secureOkWithOversizedResponseDoesNotCrashBroker() throws Exception
+    {
+        assertMalformedFrameClosesOnlyConnection(secureOkWithOversizedResponse());
+    }
+
+    @Test
+    public void startOkWithOversizedLongStringDoesNotCrashBroker() throws Exception
+    {
+        assertMalformedFrameClosesOnlyConnection(startOkWithOversizedLongString());
+    }
+
+    @Test
+    public void startOkWithOversizedLongStringInNestedTableDoesNotCrashBroker() throws Exception
+    {
+        assertMalformedFrameClosesOnlyConnection(startOkWithOversizedLongStringInNestedTable());
+    }
+
+    @Test
+    public void startOkExceedingNestingLimitClosesWithoutResponse() throws Exception
+    {
+        assertMalformedFrameClosesOnlyConnection(startOkWithExcessiveNesting());
+    }
+
+    private void assertMalformedFrameClosesOnlyConnection(final byte[] malformedFrame) throws Exception
+    {
+        try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+        {
+            final Interaction interaction = transport.newInteraction();
+            interaction.negotiateProtocol()
+                    .consumeResponse(ConnectionStartBody.class)
+                    .sendPerformative(rawFrame(malformedFrame))
+                    .consumeResponse()
+                    .getLatestResponse(ChannelClosedResponse.class);
+        }
+
+        try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+        {
+            final Interaction interaction = transport.newInteraction();
+            final ConnectionStartBody start = interaction.negotiateProtocol()
+                    .consumeResponse()
+                    .getLatestResponse(ConnectionStartBody.class);
+            assertThat(start.getVersionMajor(), is(equalTo((short) transport.getProtocolVersion().getMajorVersion())));
+        }
+    }
+
+    private static AMQDataBlock rawFrame(final byte[] bytes)
+    {
+        return new AMQDataBlock()
+        {
+            @Override
+            public long getSize()
+            {
+                return bytes.length;
+            }
+
+            @Override
+            public long writePayload(final ByteBufferSender sender)
+            {
+                sender.send(QpidByteBuffer.wrap(bytes));
+                return bytes.length;
+            }
+        };
+    }
+
+    private static byte[] secureOkWithOversizedResponse()
+    {
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(18))
+        {
+            writeFrameHeader(buffer, 10);
+            buffer.putInt(0x000A0015);              // connection.secure-ok
+            buffer.putUnsignedInt(0x20000000L);     // response length
+            buffer.putUnsignedShort(4);             // insufficient response bytes
+            buffer.put(FRAME_END);
+            return readBytes(buffer);
+        }
+    }
+
+    private static byte[] startOkWithOversizedLongString()
+    {
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(44))
+        {
+            writeFrameHeader(buffer, 36);
+            buffer.putInt(0x000A000B);              // connection.start-ok
+
+            buffer.putUnsignedInt(7);               // client-properties field-table length
+            buffer.putUnsignedByte((short) 1);
+            buffer.put((byte) 's');
+            buffer.put((byte) 'S');                 // long-string field value
+            buffer.putUnsignedInt(0x80000000L);     // field value length
+
+            writeShortString(buffer, "PLAIN");      // mechanism
+            writeLongString(buffer, "guest");       // response
+            writeShortString(buffer, "en_US");      // locale
+            buffer.put(FRAME_END);
+            return readBytes(buffer);
+        }
+    }
+
+    private static byte[] startOkWithOversizedLongStringInNestedTable()
+    {
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(57))
+        {
+            writeFrameHeader(buffer, 49);
+            buffer.putInt(0x000A000B);              // connection.start-ok
+
+            buffer.putUnsignedInt(20);              // client-properties field-table length
+            buffer.putUnsignedByte((short) 7);
+            buffer.put("ignored".getBytes(US_ASCII));
+            buffer.put((byte) 'F');                 // nested field table
+            buffer.putUnsignedInt(7);
+            buffer.putUnsignedByte((short) 1);
+            buffer.put((byte) 's');
+            buffer.put((byte) 'S');                 // long-string field value
+            buffer.putUnsignedInt(0x80000000L);     // field value length
+
+            writeShortString(buffer, "PLAIN");      // mechanism
+            writeLongString(buffer, "guest");       // response
+            writeShortString(buffer, "en_US");      // locale
+            buffer.put(FRAME_END);
+            return readBytes(buffer);
+        }
+    }
+
+    private static byte[] startOkWithExcessiveNesting()
+    {
+        final byte[] clientProperties = nestedFieldTable(MAX_NESTED_OBJECTS + 1);
+        final int bodySize = Integer.BYTES + Integer.BYTES + clientProperties.length + shortStringSize("PLAIN") +
+                longStringSize("guest") + shortStringSize("en_US");
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(8 + bodySize))
+        {
+            writeFrameHeader(buffer, bodySize);
+            buffer.putInt(0x000A000B); // connection.start-ok
+            buffer.putUnsignedInt(clientProperties.length);
+            buffer.put(clientProperties);
+            writeShortString(buffer, "PLAIN");
+            writeLongString(buffer, "guest");
+            writeShortString(buffer, "en_US");
+            buffer.put(FRAME_END);
+            return readBytes(buffer);
+        }
+    }
+
+    private static byte[] nestedFieldTable(final int depth)
+    {
+        try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(7 * depth))
+        {
+            for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+            {
+                buffer.putUnsignedByte((short) 1);
+                buffer.put((byte) 'n');
+                buffer.put((byte) 'F');
+                buffer.putUnsignedInt(7L * (remainingDepth - 1));
+            }
+            buffer.putUnsignedByte((short) 1);
+            buffer.put((byte) 'v');
+            buffer.put((byte) 'I');
+            buffer.putInt(42);
+            return readBytes(buffer);
+        }
+    }
+
+    private static void writeFrameHeader(final QpidByteBuffer buffer, final long bodySize)
+    {
+        buffer.put((byte) 1);
+        buffer.putUnsignedShort(0); // connection methods use channel zero
+        buffer.putUnsignedInt(bodySize);
+    }
+
+    private static void writeShortString(final QpidByteBuffer buffer, final String value)
+    {
+        final byte[] bytes = value.getBytes(US_ASCII);
+        buffer.putUnsignedByte((short) bytes.length);
+        buffer.put(bytes);
+    }
+
+    private static void writeLongString(final QpidByteBuffer buffer, final String value)
+    {
+        final byte[] bytes = value.getBytes(US_ASCII);
+        buffer.putUnsignedInt(bytes.length);
+        buffer.put(bytes);
+    }
+
+    private static int shortStringSize(final String value)
+    {
+        return Byte.BYTES + value.getBytes(US_ASCII).length;
+    }
+
+    private static int longStringSize(final String value)
+    {
+        return Integer.BYTES + value.getBytes(US_ASCII).length;
+    }
+
+    private static byte[] readBytes(final QpidByteBuffer buffer)
+    {
+        buffer.flip();
+        final byte[] bytes = new byte[buffer.remaining()];
+        buffer.get(bytes);
+        return bytes;
+    }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java
index 1a50ee4460..080841809d 100644
--- a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java
@@ -38,6 +38,7 @@ import org.junit.jupiter.api.Test;
 import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
 import org.apache.qpid.server.exchange.ExchangeDefaults;
 import org.apache.qpid.server.filter.AMQPFilterTypes;
+import org.apache.qpid.server.protocol.ErrorCodes;
 import org.apache.qpid.server.protocol.v0_8.AMQShortString;
 import org.apache.qpid.server.protocol.v0_8.AMQType;
 import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
@@ -50,11 +51,11 @@ import org.apache.qpid.server.protocol.v0_8.transport.BasicQosOkBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ChannelCloseOkBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ChannelFlowOkBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
 import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
 import org.apache.qpid.server.protocol.v0_8.transport.QueueBindOkBody;
 import org.apache.qpid.server.protocol.v0_8.transport.TxSelectOkBody;
-import org.apache.qpid.tests.protocol.ChannelClosedResponse;
 import org.apache.qpid.tests.protocol.v0_8.FrameTransport;
 import org.apache.qpid.tests.protocol.v0_8.Interaction;
 import org.apache.qpid.tests.utils.BrokerAdmin;
@@ -119,9 +120,8 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
                                .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
                                .contentHeaderPropertiesHeaders(malformedHeader)
                                .content(contentBytes)
-                               .publishMessage()
-                       .consumeResponse()
-                       .getLatestResponse(ChannelClosedResponse.class);
+                               .publishMessage();
+            assertConnectionRejected(transport, interaction);
 
             assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(0)));
         }
@@ -150,20 +150,20 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
                        .contentHeaderPropertiesHeaders(malformedHeader)
                        .content(contentBytes)
-                       .publishMessage()
-                       .tx().commit()
-                       .consumeResponse()
-                       .getLatestResponse(ChannelClosedResponse.class);
+                       .publishMessage();
+            assertConnectionRejected(transport, interaction);
 
             assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(0)));
         }
     }
 
     @Test
-    public void consumeMalformedMessage() throws Exception
+    public void consumeValidMessageAfterMalformedMessageRejected() throws Exception
     {
         final FieldTable malformedHeader = createHeadersWithMalformedLongString();
         final byte[] contentBytes = CONTENT_TEXT.getBytes(StandardCharsets.UTF_8);
+        publishMalformedMessage(malformedHeader, contentBytes);
+        assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(0)));
 
         final String content2 = "message2";
         final byte[] content2Bytes = content2.getBytes(StandardCharsets.UTF_8);
@@ -185,12 +185,6 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
                        .channel().flow(true)
                        .consumeResponse(ChannelFlowOkBody.class)
 
-                       .basic().publishExchange("")
-                       .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
-                       .contentHeaderPropertiesHeaders(malformedHeader)
-                       .content(contentBytes)
-                       .publishMessage()
-
                        .basic().publishExchange("")
                        .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
                        .contentHeaderPropertiesContentType("text/plain")
@@ -236,12 +230,20 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
                                .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
                                .contentHeaderPropertiesHeaders(malformedHeader)
                                .content(contentBytes)
-                               .publishMessage()
-                       .channel().close()
-                       .consumeResponse(ChannelCloseOkBody.class);
+                               .publishMessage();
+            assertConnectionRejected(transport, interaction);
         }
     }
 
+    private void assertConnectionRejected(final FrameTransport transport, final Interaction interaction) throws Exception
+    {
+        final ConnectionCloseBody close = interaction.consumeResponse(ConnectionCloseBody.class)
+                .getLatestResponse(ConnectionCloseBody.class);
+        assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.FRAME_ERROR)));
+        interaction.connection().closeOk();
+        transport.assertNoMoreResponsesAndChannelClosed();
+    }
+
     private static FieldTable createMalformedHeaders()
     {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org