Add filters before MAIN_HANDLER so they won't be ignored (#236)

diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java
index 0e71a3b..a983611 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java
@@ -396,7 +396,9 @@
                         filterChainDefinition.addFirst(entry.filterHandler, entry.name);
                         break;
                     case LAST:
-                        filterChainDefinition.addLast(entry.filterHandler, entry.name);
+                        // Don't add last, after TerminalAsyncServerFilter, as that does not delegate to the chain
+                        // Instead, add the filter just before it, making it effectively the last filter
+                        filterChainDefinition.addBefore(StandardFilter.MAIN_HANDLER.name(), entry.filterHandler, entry.name);
                         break;
                 }
             }
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java
new file mode 100644
index 0000000..8345886
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java
@@ -0,0 +1,171 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.testing.classic;
+
+import java.io.IOException;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.impl.bootstrap.HttpRequester;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap;
+import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
+import org.apache.hc.core5.http.io.HttpFilterChain;
+import org.apache.hc.core5.http.io.HttpFilterHandler;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.Timeout;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClassicServerBootstrapFilterTest {
+
+    private static final Timeout TIMEOUT = Timeout.ofSeconds(30);
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private HttpServer server;
+
+    @Rule
+    public ExternalResource serverResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            log.debug("Starting up test server");
+            server = ServerBootstrap.bootstrap()
+                    .setSocketConfig(SocketConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .build())
+                    .register("*", new EchoHandler())
+                    .addFilterLast("test-filter", new HttpFilterHandler() {
+
+                        @Override
+                        public void handle(
+                                final ClassicHttpRequest request,
+                                final HttpFilterChain.ResponseTrigger responseTrigger,
+                                final HttpContext context,
+                                final HttpFilterChain chain) throws HttpException, IOException {
+                            chain.proceed(request, new HttpFilterChain.ResponseTrigger() {
+
+                                @Override
+                                public void sendInformation(
+                                        final ClassicHttpResponse response) throws HttpException, IOException {
+                                    responseTrigger.sendInformation(response);
+                                }
+
+                                @Override
+                                public void submitResponse(
+                                        final ClassicHttpResponse response) throws HttpException, IOException {
+                                    response.setHeader("X-Test-Filter", "active");
+                                    responseTrigger.submitResponse(response);
+                                }
+
+                            }, context);
+                        }
+
+                    })
+                    .setExceptionListener(LoggingExceptionListener.INSTANCE)
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE)
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            log.debug("Shutting down test server");
+            if (server != null) {
+                try {
+                    server.close(CloseMode.IMMEDIATE);
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    private HttpRequester requester;
+
+    @Rule
+    public ExternalResource clientResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            log.debug("Starting up test client");
+            requester = RequesterBootstrap.bootstrap()
+                    .setSocketConfig(SocketConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .build())
+                    .setMaxTotal(2)
+                    .setDefaultMaxPerRoute(2)
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE)
+                    .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            log.debug("Shutting down test client");
+            if (requester != null) {
+                try {
+                    requester.close(CloseMode.GRACEFUL);
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    @Test
+    public void testFilters() throws Exception {
+        server.start();
+        final HttpHost target = new HttpHost("http", "localhost", server.getLocalPort());
+        final HttpCoreContext context = HttpCoreContext.create();
+        final ClassicHttpRequest request = new BasicClassicHttpRequest(Method.POST, "/filters");
+        request.setEntity(new StringEntity("some stuff", ContentType.TEXT_PLAIN));
+        try (final ClassicHttpResponse response = requester.execute(target, request, TIMEOUT, context)) {
+            MatcherAssert.assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK));
+            final Header testFilterHeader = response.getHeader("X-Test-Filter");
+            MatcherAssert.assertThat(testFilterHeader, CoreMatchers.notNullValue());
+        }
+    }
+
+}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java
new file mode 100644
index 0000000..ed60fbd
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java
@@ -0,0 +1,209 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.testing.nio;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Future;
+
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.Message;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap;
+import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap;
+import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
+import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
+import org.apache.hc.core5.http.nio.AsyncDataConsumer;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.nio.AsyncFilterChain;
+import org.apache.hc.core5.http.nio.AsyncFilterHandler;
+import org.apache.hc.core5.http.nio.AsyncPushProducer;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
+import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
+import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
+import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.UriPatternMatcher;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.reactor.ListenerEndpoint;
+import org.apache.hc.core5.testing.SSLTestContexts;
+import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
+import org.apache.hc.core5.util.Timeout;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AsyncServerBootstrapFilterTest {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final Timeout TIMEOUT = Timeout.ofSeconds(30);
+
+    private HttpAsyncServer server;
+
+    @Rule
+    public ExternalResource serverResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            log.debug("Starting up test server");
+            server = AsyncServerBootstrap.bootstrap()
+                    .setLookupRegistry(new UriPatternMatcher<Supplier<AsyncServerExchangeHandler>>())
+                    .setIOReactorConfig(
+                            IOReactorConfig.custom()
+                                    .setSoTimeout(TIMEOUT)
+                                    .build())
+                    .register("*", new Supplier<AsyncServerExchangeHandler>() {
+
+                        @Override
+                        public AsyncServerExchangeHandler get() {
+                            return new EchoHandler(2048);
+                        }
+
+                    })
+                    .addFilterLast("test-filter", new AsyncFilterHandler() {
+
+                        @Override
+                        public AsyncDataConsumer handle(
+                                final HttpRequest request,
+                                final EntityDetails entityDetails,
+                                final HttpContext context,
+                                final AsyncFilterChain.ResponseTrigger responseTrigger,
+                                final AsyncFilterChain chain) throws HttpException, IOException {
+                            return chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() {
+
+                                @Override
+                                public void sendInformation(
+                                        final HttpResponse response) throws HttpException, IOException {
+                                    responseTrigger.sendInformation(response);
+                                }
+
+                                @Override
+                                public void submitResponse(
+                                        final HttpResponse response,
+                                        final AsyncEntityProducer entityProducer) throws HttpException, IOException {
+                                    response.setHeader("X-Test-Filter", "active");
+                                    responseTrigger.submitResponse(response, entityProducer);
+                                }
+
+                                @Override
+                                public void pushPromise(
+                                        final HttpRequest promise,
+                                        final AsyncPushProducer responseProducer) throws HttpException, IOException {
+                                    responseTrigger.pushPromise(promise, responseProducer);
+                                }
+
+                            });
+                        }
+                    })
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
+                    .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
+                    .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
+                    .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            log.debug("Shutting down test server");
+            if (server != null) {
+                server.close(CloseMode.GRACEFUL);
+            }
+        }
+
+    };
+
+    private HttpAsyncRequester requester;
+
+    @Rule
+    public ExternalResource clientResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            log.debug("Starting up test client");
+            requester = AsyncRequesterBootstrap.bootstrap()
+                    .setIOReactorConfig(IOReactorConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .build())
+                    .setTlsStrategy(new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
+                    .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
+                    .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
+                    .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
+                    .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            log.debug("Shutting down test client");
+            if (requester != null) {
+                requester.close(CloseMode.GRACEFUL);
+            }
+        }
+
+    };
+
+    @Test
+    public void testFilters() throws Exception {
+        server.start();
+        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTP);
+        final ListenerEndpoint listener = future.get();
+        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
+        requester.start();
+
+        final HttpHost target = new HttpHost("http", "localhost", address.getPort());
+        final Future<Message<HttpResponse, String>> resultFuture = requester.execute(
+                new BasicRequestProducer(Method.POST, target, "/filters",
+                        new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
+                new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
+        final Message<HttpResponse, String> message = resultFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+        MatcherAssert.assertThat(message, CoreMatchers.notNullValue());
+        final HttpResponse response = message.getHead();
+        MatcherAssert.assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK));
+        final Header testFilterHeader = response.getHeader("X-Test-Filter");
+        MatcherAssert.assertThat(testFilterHeader, CoreMatchers.notNullValue());
+    }
+
+}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java
new file mode 100644
index 0000000..e629fb0
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java
@@ -0,0 +1,214 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.testing.nio;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Future;
+
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.Message;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
+import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
+import org.apache.hc.core5.http.nio.AsyncDataConsumer;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.nio.AsyncFilterChain;
+import org.apache.hc.core5.http.nio.AsyncFilterHandler;
+import org.apache.hc.core5.http.nio.AsyncPushProducer;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
+import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
+import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.UriPatternMatcher;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap;
+import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap;
+import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.reactor.ListenerEndpoint;
+import org.apache.hc.core5.testing.SSLTestContexts;
+import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
+import org.apache.hc.core5.util.Timeout;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class H2ServerBootstrapFiltersTest {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final Timeout TIMEOUT = Timeout.ofSeconds(30);
+
+    private HttpAsyncServer server;
+
+    @Rule
+    public ExternalResource serverResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            log.debug("Starting up test server");
+            server = H2ServerBootstrap.bootstrap()
+                    .setLookupRegistry(new UriPatternMatcher<Supplier<AsyncServerExchangeHandler>>())
+                    .setVersionPolicy(HttpVersionPolicy.NEGOTIATE)
+                    .setIOReactorConfig(
+                            IOReactorConfig.custom()
+                                    .setSoTimeout(TIMEOUT)
+                                    .build())
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
+                    .setStreamListener(LoggingH2StreamListener.INSTANCE)
+                    .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
+                    .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
+                    .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
+                    .register("*", new Supplier<AsyncServerExchangeHandler>() {
+
+                        @Override
+                        public AsyncServerExchangeHandler get() {
+                            return new EchoHandler(2048);
+                        }
+
+                    })
+                    .addFilterLast("test-filter", new AsyncFilterHandler() {
+
+                        @Override
+                        public AsyncDataConsumer handle(
+                                final HttpRequest request,
+                                final EntityDetails entityDetails,
+                                final HttpContext context,
+                                final AsyncFilterChain.ResponseTrigger responseTrigger,
+                                final AsyncFilterChain chain) throws HttpException, IOException {
+                            return chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() {
+
+                                @Override
+                                public void sendInformation(
+                                        final HttpResponse response) throws HttpException, IOException {
+                                    responseTrigger.sendInformation(response);
+                                }
+
+                                @Override
+                                public void submitResponse(
+                                        final HttpResponse response,
+                                        final AsyncEntityProducer entityProducer) throws HttpException, IOException {
+                                    response.setHeader("X-Test-Filter", "active");
+                                    responseTrigger.submitResponse(response, entityProducer);
+                                }
+
+                                @Override
+                                public void pushPromise(
+                                        final HttpRequest promise,
+                                        final AsyncPushProducer responseProducer) throws HttpException, IOException {
+                                    responseTrigger.pushPromise(promise, responseProducer);
+                                }
+
+                            });
+                        }
+                    })
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            log.debug("Shutting down test server");
+            if (server != null) {
+                server.close(CloseMode.GRACEFUL);
+            }
+        }
+
+    };
+
+    private HttpAsyncRequester requester;
+
+    @Rule
+    public ExternalResource clientResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            log.debug("Starting up test client");
+            requester = H2RequesterBootstrap.bootstrap()
+                    .setVersionPolicy(HttpVersionPolicy.NEGOTIATE)
+                    .setIOReactorConfig(IOReactorConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .build())
+                    .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
+                    .setStreamListener(LoggingH2StreamListener.INSTANCE)
+                    .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
+                    .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
+                    .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
+                    .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            log.debug("Shutting down test client");
+            if (requester != null) {
+                requester.close(CloseMode.GRACEFUL);
+            }
+        }
+
+    };
+
+    @Test
+    public void testSequentialRequests() throws Exception {
+        server.start();
+        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTP);
+        final ListenerEndpoint listener = future.get();
+        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
+        requester.start();
+
+        final HttpHost target = new HttpHost("http", "localhost", address.getPort());
+        final Future<Message<HttpResponse, String>> resultFuture = requester.execute(
+                new BasicRequestProducer(Method.POST, target, "/stuff",
+                        new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
+                new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
+        final Message<HttpResponse, String> message = resultFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+        MatcherAssert.assertThat(message, CoreMatchers.notNullValue());
+        final HttpResponse response = message.getHead();
+        MatcherAssert.assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK));
+        final Header testFilterHeader = response.getHeader("X-Test-Filter");
+        MatcherAssert.assertThat(testFilterHeader, CoreMatchers.notNullValue());
+    }
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java
index 4eb47af..c5041bb 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java
@@ -373,7 +373,9 @@
                         filterChainDefinition.addFirst(entry.filterHandler, entry.name);
                         break;
                     case LAST:
-                        filterChainDefinition.addLast(entry.filterHandler, entry.name);
+                        // Don't add last, after TerminalAsyncServerFilter, as that does not delegate to the chain
+                        // Instead, add the filter just before it, making it effectively the last filter
+                        filterChainDefinition.addBefore(StandardFilter.MAIN_HANDLER.name(), entry.filterHandler, entry.name);
                         break;
                 }
             }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java
index dbf5c20..8e445c0 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java
@@ -365,7 +365,9 @@
                         filterChainDefinition.addFirst(entry.filterHandler, entry.name);
                         break;
                     case LAST:
-                        filterChainDefinition.addLast(entry.filterHandler, entry.name);
+                        // Don't add last, after TerminalServerFilter, as that does not delegate to the chain
+                        // Instead, add the filter just before it, making it effectively the last filter
+                        filterChainDefinition.addBefore(StandardFilter.MAIN_HANDLER.name(), entry.filterHandler, entry.name);
                         break;
                 }
             }