diff --git a/http4s-backend/src/main/scala/sttp/client4/http4s/Http4sBackendBase.scala b/http4s-backend/src/main/scala/sttp/client4/http4s/Http4sBackendBase.scala index 5d302da95b..ca88ff320a 100644 --- a/http4s-backend/src/main/scala/sttp/client4/http4s/Http4sBackendBase.scala +++ b/http4s-backend/src/main/scala/sttp/client4/http4s/Http4sBackendBase.scala @@ -111,9 +111,13 @@ private[http4s] abstract class Http4sBackendBase[F[_]](implicit protected val as } .recoverWith { case t: Throwable => responseVar.complete(Left(t)).as(()) } - sendRequest.start >> responseVar.get.flatMap { - case Left(t) => implicitly[cats.ApplicativeError[F, Throwable]].raiseError(t) - case Right(r) => r.pure[F] + sendRequest.start.flatMap { fiber => + responseVar.get + .onCancel(fiber.cancel) + .flatMap { + case Left(t) => implicitly[cats.ApplicativeError[F, Throwable]].raiseError(t) + case Right(r) => r.pure[F] + } } } } diff --git a/http4s-backend/src/test/scalajvm/sttp/client4/http4s/Http4sBackendCancellationTest.scala b/http4s-backend/src/test/scalajvm/sttp/client4/http4s/Http4sBackendCancellationTest.scala new file mode 100644 index 0000000000..6bdc2084d2 --- /dev/null +++ b/http4s-backend/src/test/scalajvm/sttp/client4/http4s/Http4sBackendCancellationTest.scala @@ -0,0 +1,36 @@ +package sttp.client4.http4s + +import cats.effect.{Deferred, IO} +import cats.effect.unsafe.IORuntime +import org.http4s.{Response => Http4sResponse} +import org.http4s.client.Client +import org.scalatest.flatspec.AsyncFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.client4._ + +import scala.concurrent.duration._ + +class Http4sBackendCancellationTest extends AsyncFlatSpec with Matchers { + + implicit val ioRuntime: IORuntime = IORuntime.global + + it should "cancel the underlying request fiber when the caller cancels" in { + val test = for { + cancelled <- Deferred[IO, Unit] + // A client whose request never completes, but records when it is cancelled + client = Client[IO] { _ => + IO.never[Http4sResponse[IO]] + .onCancel(cancelled.complete(()).void) + .toResource + } + backend = Http4sBackend.usingClient[IO](client) + req = basicRequest.get(uri"http://localhost/test").response(asString) + // Send the request, then cancel it after a short delay + _ <- req.send(backend).void.timeoutTo(50.millis, IO.unit) + // If the fiber was properly cancelled, onCancel will have signalled + _ <- cancelled.get.timeout(3.seconds) + } yield succeed + + test.unsafeToFuture() + } +}