From ea888ac0b5c51199b6c95a53cc4443f348c84162 Mon Sep 17 00:00:00 2001 From: Erlend Hamnaberg Date: Mon, 26 Feb 2024 13:15:42 +0100 Subject: [PATCH] Fix idle timeout Incorrectly assumed that netty would throw an Exception --- .../http4s/netty/client/Http4sHandler.scala | 6 +- .../client/NettyClientIdleTimeoutTest.scala | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 client/src/test/scala/org/http4s/netty/client/NettyClientIdleTimeoutTest.scala diff --git a/client/src/main/scala/org/http4s/netty/client/Http4sHandler.scala b/client/src/main/scala/org/http4s/netty/client/Http4sHandler.scala index a02e5c4c..dd249bcf 100644 --- a/client/src/main/scala/org/http4s/netty/client/Http4sHandler.scala +++ b/client/src/main/scala/org/http4s/netty/client/Http4sHandler.scala @@ -32,6 +32,7 @@ import java.io.IOException import java.nio.channels.ClosedChannelException import scala.concurrent.ExecutionContext import scala.concurrent.Future +import scala.concurrent.TimeoutException import scala.util.Failure import scala.util.Success @@ -200,8 +201,9 @@ private[netty] class Http4sHandler[F[_]](dispatcher: Dispatcher[F])(implicit F: override def userEventTriggered(ctx: ChannelHandlerContext, evt: scala.Any): Unit = void { evt match { case _: IdleStateEvent if ctx.channel().isOpen => - logger.trace(s"Closing connection due to idle timeout") - ctx.channel().close() + val message = s"Closing connection due to idle timeout" + logger.trace(message) + onException(ctx.channel(), new TimeoutException(message)) case _ => super.userEventTriggered(ctx, evt) } } diff --git a/client/src/test/scala/org/http4s/netty/client/NettyClientIdleTimeoutTest.scala b/client/src/test/scala/org/http4s/netty/client/NettyClientIdleTimeoutTest.scala new file mode 100644 index 00000000..28891d45 --- /dev/null +++ b/client/src/test/scala/org/http4s/netty/client/NettyClientIdleTimeoutTest.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2020 http4s.org + * + * 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.http4s.netty.client + +import cats.effect.IO +import com.comcast.ip4s._ +import munit.catseffect.IOFixture +import org.http4s.HttpRoutes +import org.http4s.Request +import org.http4s.Response +import org.http4s.client.Client +import org.http4s.dsl.io._ +import org.http4s.ember.server.EmberServerBuilder +import org.http4s.implicits._ +import org.http4s.server.Server + +import scala.concurrent.duration._ + +class NettyClientIdleTimeoutTest extends IOSuite { + override val munitIOTimeout: Duration = 1.minute + + val nettyClient: IOFixture[Client[IO]] = + resourceFixture( + NettyClientBuilder[IO] + .withIdleTimeout(2.seconds) + .resource, + "netty client") + + val server: IOFixture[Server] = resourceFixture( + EmberServerBuilder + .default[IO] + .withPort(port"0") + .withHttpApp( + HttpRoutes + .of[IO] { case GET -> Root / "idle-timeout" => + IO.sleep(30.seconds).as(Response()) + } + .orNotFound + ) + .build, + "server" + ) + + List( + (nettyClient, "netty client") + ).foreach { case (client, name) => + test(s"$name fails after idle timeout") { + val s = server() + + val req = Request[IO](uri = s.baseUri / "idle-timeout") + val response = client().run(req).allocated.attempt + IO.race(response, IO.sleep(5.seconds)).map { + case Left(Left(error)) => println(s"response failed, error:"); error.printStackTrace() + case Left(Right(_)) => println("response available") + case Right(_) => fail("idle timeout wasn't triggered") + } + } + } +}