Http2PingHandler.java

/*
 *    Copyright (c) 2014-2026 AsyncHttpClient Project. All rights reserved.
 *
 *    Licensed 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.asynchttpclient.netty.handler;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.DefaultHttp2PingFrame;
import io.netty.handler.codec.http2.Http2PingFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * Sends HTTP/2 PING frames when the connection is idle and closes the connection
 * if no PING ACK is received within the timeout period.
 */
public class Http2PingHandler extends ChannelDuplexHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(Http2PingHandler.class);
    private static final long PING_ACK_TIMEOUT_MS = 5000;

    private boolean waitingForPingAck;
    private ScheduledFuture<?> pingAckTimeoutFuture;

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleEvent = (IdleStateEvent) evt;
            if (idleEvent.state() == IdleState.ALL_IDLE && !waitingForPingAck) {
                waitingForPingAck = true;
                LOGGER.debug("Sending HTTP/2 PING on idle connection {}", ctx.channel());
                ctx.writeAndFlush(new DefaultHttp2PingFrame(System.nanoTime(), false));

                pingAckTimeoutFuture = ctx.executor().schedule(() -> {
                    if (waitingForPingAck) {
                        LOGGER.debug("PING ACK timeout on connection {}, closing", ctx.channel());
                        ctx.close();
                    }
                }, PING_ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            }
        }
        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof Http2PingFrame) {
            Http2PingFrame pingFrame = (Http2PingFrame) msg;
            if (pingFrame.ack()) {
                waitingForPingAck = false;
                if (pingAckTimeoutFuture != null) {
                    pingAckTimeoutFuture.cancel(false);
                    pingAckTimeoutFuture = null;
                }
                LOGGER.debug("Received PING ACK on connection {}", ctx.channel());
                return; // consume the PING ACK
            }
        }
        super.channelRead(ctx, msg);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        if (pingAckTimeoutFuture != null) {
            pingAckTimeoutFuture.cancel(false);
            pingAckTimeoutFuture = null;
        }
    }
}