diff --git a/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala b/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala index c34aa662e..840ebb498 100644 --- a/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala +++ b/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala @@ -144,12 +144,7 @@ object ResolutionRun { resolveTask.io.attempt .flatMap { case Left(e: ResolutionError) => - val hasConnectionTimeouts = e.errors.exists { - case err: CantDownloadModule => - err.perRepositoryErrors.exists(_.contains("Connection timed out")) - case _ => false - } - if (hasConnectionTimeouts) + if (isTransientResolutionError(e)) if (attempt + 1 >= maxAttempts) { log.error(s"Failed, maximum iterations ($maxAttempts) reached") Task.point(Left(e)) @@ -280,4 +275,16 @@ object ResolutionRun { } private lazy val retryScheduler = ThreadUtil.fixedScheduledThreadPool(1) + + private[internal] def isTransientResolutionError(e: ResolutionError): Boolean = + e.errors.exists { + case err: CantDownloadModule => isTimeout(err) || isServerError(err) + case _ => false + } + + private def isTimeout(err: CantDownloadModule): Boolean = + err.perRepositoryErrors.exists(_.contains("Connection timed out")) + + private def isServerError(err: CantDownloadModule): Boolean = + err.perRepositoryErrors.exists(_.contains("Server returned HTTP response code: 5")) } diff --git a/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala b/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala new file mode 100644 index 000000000..9b00657b3 --- /dev/null +++ b/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala @@ -0,0 +1,38 @@ +package lmcoursier.internal + +import coursier.core.{ Module, ModuleName, Organization, Resolution } +import coursier.error.ResolutionError.CantDownloadModule +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class ResolutionRunSpec extends AnyFunSuite with Matchers: + + private def cantDownload(errors: String*): CantDownloadModule = + new CantDownloadModule( + Resolution(), + Module(Organization("org"), ModuleName("mod"), Map.empty), + "1.0", + errors.toSeq + ) + + test("503 is a transient resolution error"): + val err = cantDownload( + "Server returned HTTP response code: 503 for URL: https://repo.example.com/org/mod/1.0/mod-1.0.pom" + ) + ResolutionRun.isTransientResolutionError(err) shouldBe true + + test("500 is a transient resolution error"): + val err = cantDownload( + "Server returned HTTP response code: 500 for URL: https://repo.example.com/org/mod/1.0/mod-1.0.pom" + ) + ResolutionRun.isTransientResolutionError(err) shouldBe true + + test("connection timeout is a transient resolution error"): + val err = cantDownload("Connection timed out") + ResolutionRun.isTransientResolutionError(err) shouldBe true + + test("404 is not a transient resolution error"): + val err = cantDownload( + "Server returned HTTP response code: 404 for URL: https://repo.example.com/org/mod/1.0/mod-1.0.pom" + ) + ResolutionRun.isTransientResolutionError(err) shouldBe false