From b2db55768c375a4074dbf63d0e67b030fc7d42aa Mon Sep 17 00:00:00 2001 From: MkDev11 Date: Thu, 15 Jan 2026 00:23:31 -0500 Subject: [PATCH] [2.x] fix: Log server response body on publish failure (#8537) When publishing to a repository fails with an HTTP error (e.g., 403, 409), the server often includes helpful error details in the response body. Previously, sbt only showed the HTTP status code without the response body. This reimplements the upload method. Fixes #7423 --- .../ErrorLoggingURLHandler.scala | 75 +++++++++++++++++++ .../sbt/internal/librarymanagement/Ivy.scala | 2 +- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 lm-ivy/src/main/scala/sbt/internal/librarymanagement/ErrorLoggingURLHandler.scala diff --git a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/ErrorLoggingURLHandler.scala b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/ErrorLoggingURLHandler.scala new file mode 100644 index 000000000..8da55968a --- /dev/null +++ b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/ErrorLoggingURLHandler.scala @@ -0,0 +1,75 @@ +/* sbt -- Simple Build Tool + * Copyright 2008, 2009, 2010 Mark Harrah + */ +package sbt.internal.librarymanagement + +import java.io.{ File, FileInputStream, IOException } +import java.net.{ HttpURLConnection, URL } +import org.apache.ivy.util.url.{ BasicURLHandler, IvyAuthenticator } +import org.apache.ivy.util.{ CopyProgressListener, FileUtil, Message } +import org.apache.ivy.Ivy +import scala.io.Source +import scala.util.Using + +private[librarymanagement] class ErrorLoggingURLHandler extends BasicURLHandler { + private val ErrorBodyTruncateLen = 1024 + + override def upload( + source: File, + dest: URL, + l: CopyProgressListener + ): Unit = { + if (dest.getProtocol != "http" && dest.getProtocol != "https") { + throw new UnsupportedOperationException( + "URL repository only support HTTP PUT at the moment" + ) + } + + IvyAuthenticator.install() + + var conn: HttpURLConnection = null + try { + val normalizedDest = normalizeToURL(dest) + conn = normalizedDest.openConnection().asInstanceOf[HttpURLConnection] + conn.setDoOutput(true) + conn.setRequestMethod("PUT") + conn.setRequestProperty("User-Agent", "Apache Ivy/" + Ivy.getIvyVersion) + conn.setRequestProperty( + "Accept", + "application/octet-stream, application/json, application/xml, */*" + ) + conn.setRequestProperty("Content-type", "application/octet-stream") + conn.setRequestProperty("Content-length", source.length().toString) + conn.setInstanceFollowRedirects(true) + + val in = new FileInputStream(source) + try { + val os = conn.getOutputStream + FileUtil.copy(in, os, l) + } finally { + try in.close() + catch { case _: IOException => } + } + + val responseCode = conn.getResponseCode + val responseMessage = conn.getResponseMessage + + val errorBody = Option(conn.getErrorStream).map { stream => + Using.resource(stream) { s => + val body = Source.fromInputStream(s, "UTF-8").mkString + if (body.length > ErrorBodyTruncateLen) + body.take(ErrorBodyTruncateLen) + "..." + else body + } + } + + errorBody.filter(_.nonEmpty).foreach { body => + Message.error(s"Server response body: $body") + } + + validatePutStatusCode(dest, responseCode, responseMessage) + } finally { + if (conn != null) conn.disconnect() + } + } +} diff --git a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala index 329444304..fcec2c721 100644 --- a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala +++ b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala @@ -85,7 +85,7 @@ final class IvySbt( } } - private lazy val basicUrlHandler: URLHandler = new BasicURLHandler + private lazy val basicUrlHandler: URLHandler = new ErrorLoggingURLHandler private lazy val settings: IvySettings = { val dispatcher: URLHandlerDispatcher = URLHandlerRegistry.getDefault match {