fix: don't fail resolution when an artifact has no sources jar (#1614)

When fetch_sources = True and an artifact publishes a main jar but no
sources jar (for example the empty com.google.guava:listenablefuture jar
that Guava depends on), the Maven and Gradle resolvers' shared download
phase threw UriNotFoundException instead of treating the sources jar as
absent, failing the whole resolution.

Downloader.download now returns null for a missing sources/javadoc jar so
the caller can skip it, matching the `if (sdm != null)` guard already in
AbstractMain.getDependencyInfos.

Adds a DownloaderTest unit reproducer plus end-to-end guava installs
exercised via both the Maven and Gradle resolvers.
diff --git a/MODULE.bazel b/MODULE.bazel
index c3e1bbf..f9dd02b 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -940,6 +940,41 @@
     fetch_sources = True,
     lock_file = "//tests/custom_maven_install:transitive_dependency_with_type_of_pom.json",
 )
+
+# https://github.com/bazel-contrib/rules_jvm_external/issues/1477
+dev_maven.install(
+    name = "maven_missing_source_jar",
+    artifacts = [
+        # Guava depends on the empty
+        # com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
+        # jar, which publishes a main jar but no sources jar. With the Maven
+        # resolver and fetch_sources = True this used to fail resolution rather
+        # than treat the missing sources jar as absent.
+        "com.google.guava:guava:33.4.8-jre",
+    ],
+    fetch_sources = True,
+    lock_file = "//tests/custom_maven_install:maven_missing_source_jar_install.json",
+    repositories = [
+        "https://repo1.maven.org/maven2",
+    ],
+    resolver = "maven",
+)
+
+# https://github.com/bazel-contrib/rules_jvm_external/issues/1477
+# Same missing-sources-jar case as maven_missing_source_jar, but exercised via
+# the Gradle resolver.
+dev_maven.install(
+    name = "gradle_missing_source_jar",
+    artifacts = [
+        "com.google.guava:guava:33.4.8-jre",
+    ],
+    fetch_sources = True,
+    lock_file = "//tests/custom_maven_install:gradle_missing_source_jar_install.json",
+    repositories = [
+        "https://repo1.maven.org/maven2",
+    ],
+    resolver = "gradle",
+)
 dev_maven.install(
     name = "pom_exclusion_testing_coursier",
     artifacts = [],
@@ -1120,6 +1155,14 @@
     "transitive_dependency_with_type_of_pom",
     "unpinned_transitive_dependency_with_type_of_pom",
 
+    # Pinned repo
+    "maven_missing_source_jar",
+    "unpinned_maven_missing_source_jar",
+
+    # Pinned repo
+    "gradle_missing_source_jar",
+    "unpinned_gradle_missing_source_jar",
+
     # Regression testing and libraries exposed as compat repos
     "com_android_support_appcompat_v7_aar_28_0_0",
     "com_google_guava_guava_27_0_jre",
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java
index d28e89c..4b3cc59 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java
@@ -92,6 +92,13 @@
       return result;
     }
 
+    // Source and javadoc jars are optional: many artifacts publish a main jar but no sources jar
+    // (for example the empty com.google.guava:listenablefuture jar Guava depends on). Signal their
+    // absence with null so callers can skip them instead of failing the whole resolution.
+    if (NO_FALLBACK_CLASSIFIERS.contains(coords.getClassifier())) {
+      return null;
+    }
+
     // Are we dealing with a packaging dep? Download the `pom.xml` and check
     String originalTarget = coords.toRepoPath();
     String pomName = String.format("%s-%s.pom", coords.getArtifactId(), coords.getVersion());
diff --git a/tests/bazel_run_tests.sh b/tests/bazel_run_tests.sh
index 26a624d..655edef 100755
--- a/tests/bazel_run_tests.sh
+++ b/tests/bazel_run_tests.sh
@@ -285,6 +285,34 @@
   expect_file_is_not_empty "tests/custom_maven_install/bom_only_pinning_install.json"
 }
 
+function test_maven_resolver_missing_source_jar() {
+  # https://github.com/bazel-contrib/rules_jvm_external/issues/1477
+  # Guava depends on the empty listenablefuture jar, which has no sources jar.
+  # Resolving with the Maven resolver and fetch_sources = True must not fail on
+  # the missing sources jar (this `bazel run` crashed before the fix).
+  REPIN=1 bazel run @maven_missing_source_jar//:pin >> "$TEST_LOG" 2>&1
+  expect_file_is_not_empty "tests/custom_maven_install/maven_missing_source_jar_install.json"
+
+  bazel query '@maven_missing_source_jar//:*' >> "$TEST_LOG" 2>&1
+  # Sources were fetched for artifacts that publish them...
+  expect_log "guava.*-sources.jar"
+  # ...but the source-less listenablefuture jar has no sources target.
+  expect_not_log "listenablefuture.*sources"
+}
+
+function test_gradle_resolver_missing_source_jar() {
+  # https://github.com/bazel-contrib/rules_jvm_external/issues/1477
+  # As test_maven_resolver_missing_source_jar, but via the Gradle resolver.
+  REPIN=1 bazel run @gradle_missing_source_jar//:pin >> "$TEST_LOG" 2>&1
+  expect_file_is_not_empty "tests/custom_maven_install/gradle_missing_source_jar_install.json"
+
+  bazel query '@gradle_missing_source_jar//:*' >> "$TEST_LOG" 2>&1
+  # Sources were fetched for artifacts that publish them...
+  expect_log "guava.*-sources.jar"
+  # ...but the source-less listenablefuture jar has no sources target.
+  expect_not_log "listenablefuture.*sources"
+}
+
 function test_coursier_resolution_with_boms() {
     # Only run for Bazel 7 or above
     RELEASE="$(bazel info release | sed -e 's/release //' | cut -d '.' -f 1)"
@@ -440,6 +468,8 @@
   "test_v1_lock_file_format"
   "test_dependency_pom_exclusion"
   "test_transitive_dependency_with_type_of_pom"
+  "test_maven_resolver_missing_source_jar"
+  "test_gradle_resolver_missing_source_jar"
   "test_when_both_pom_and_jar_artifact_are_available_jar_artifact_is_present"
   "test_when_both_pom_and_jar_artifact_are_dependencies_jar_artifact_is_present"
   "test_publish_java_binary_jar_with_maven_export"
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java
index 6eb4483..81f943c 100644
--- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java
+++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java
@@ -15,6 +15,7 @@
 package com.github.bazelbuild.rules_jvm_external.resolver.maven;
 
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.github.bazelbuild.rules_jvm_external.Coordinates;
@@ -100,4 +101,30 @@
     assertTrue(downloadResult.getPath().isPresent());
     assertTrue(Files.exists(destination));
   }
+
+  @Test
+  public void missingSourceJarReturnsNullRatherThanThrowing() throws IOException {
+    // Some artifacts (for example, the empty
+    // com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava jar pulled in by
+    // Guava) publish a main jar but no sources jar. Requesting the missing sources jar should
+    // signal absence by returning null, not fail the whole resolution.
+    Coordinates coords = new Coordinates("com.example:no-sources:1.0");
+
+    Path repo = MavenRepo.create().add(coords).getPath();
+    Path localRepo = Files.createTempDirectory("local");
+
+    Coordinates sourceCoords = coords.setClassifier("sources").setExtension("jar");
+
+    DownloadResult downloadResult =
+        new Downloader(
+                Netrc.fromUserHome(),
+                localRepo,
+                Set.of(repo.toUri()),
+                new NullListener(),
+                false,
+                Map.of())
+            .download(sourceCoords);
+
+    assertNull(downloadResult);
+  }
 }
diff --git a/tests/custom_maven_install/gradle_missing_source_jar_install.json b/tests/custom_maven_install/gradle_missing_source_jar_install.json
new file mode 100644
index 0000000..2a4a92a
--- /dev/null
+++ b/tests/custom_maven_install/gradle_missing_source_jar_install.json
@@ -0,0 +1,125 @@
+{
+  "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
+  "__INPUT_ARTIFACTS_HASH": {
+    "com.google.guava:guava": -1316430090,
+    "repositories": -1949687017
+  },
+  "__RESOLVED_ARTIFACTS_HASH": {
+    "com.google.errorprone:error_prone_annotations": 1014409728,
+    "com.google.errorprone:error_prone_annotations:jar:sources": 2059442212,
+    "com.google.guava:failureaccess": 1715931538,
+    "com.google.guava:failureaccess:jar:sources": 1303858893,
+    "com.google.guava:guava": -2046459454,
+    "com.google.guava:guava:jar:sources": 544801871,
+    "com.google.guava:listenablefuture": 1079558157,
+    "com.google.j2objc:j2objc-annotations": -404209759,
+    "com.google.j2objc:j2objc-annotations:jar:sources": 1852793670,
+    "org.jspecify:jspecify": 117231129,
+    "org.jspecify:jspecify:jar:sources": -2134060298
+  },
+  "artifacts": {
+    "com.google.errorprone:error_prone_annotations": {
+      "shasums": {
+        "jar": "77440e270b0bc9a249903c5a076c36a722c4886ca4f42675f2903a1c53ed61a5",
+        "sources": "7e117e0931cb2cb4226372af336189b49edb79969d120ec958a6df0beacb0612"
+      },
+      "version": "2.36.0"
+    },
+    "com.google.guava:failureaccess": {
+      "shasums": {
+        "jar": "cbfc3906b19b8f55dd7cfd6dfe0aa4532e834250d7f080bd8d211a3e246b59cb",
+        "sources": "6fef4dfd2eb9f961655f2a3c4ea87c023618d9fcbfb6b104c17862e5afe66b97"
+      },
+      "version": "1.0.3"
+    },
+    "com.google.guava:guava": {
+      "shasums": {
+        "jar": "f3d7f57f67fd622f4d468dfdd692b3a5e3909246c28017ac3263405f0fe617ed",
+        "sources": "9d3c6aad893daac9d4812eb9fa4c3f7956a9f2e472eb7df2fea0e467fed7e766"
+      },
+      "version": "33.4.8-jre"
+    },
+    "com.google.guava:listenablefuture": {
+      "shasums": {
+        "jar": "b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99"
+      },
+      "version": "9999.0-empty-to-avoid-conflict-with-guava"
+    },
+    "com.google.j2objc:j2objc-annotations": {
+      "shasums": {
+        "jar": "88241573467ddca44ffd4d74aa04c2bbfd11bf7c17e0c342c94c9de7a70a7c64",
+        "sources": "bd60019a0423c3a025ef6ab24fe0761f5f45ffb48a8cca74a01b678de1105d38"
+      },
+      "version": "3.0.0"
+    },
+    "org.jspecify:jspecify": {
+      "shasums": {
+        "jar": "1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab",
+        "sources": "adf0898191d55937fb3192ba971826f4f294292c4a960740f3c27310e7b70296"
+      },
+      "version": "1.0.0"
+    }
+  },
+  "dependencies": {
+    "com.google.guava:guava": [
+      "com.google.errorprone:error_prone_annotations",
+      "com.google.guava:failureaccess",
+      "com.google.guava:listenablefuture",
+      "com.google.j2objc:j2objc-annotations",
+      "org.jspecify:jspecify"
+    ]
+  },
+  "packages": {
+    "com.google.errorprone:error_prone_annotations": [
+      "com.google.errorprone.annotations",
+      "com.google.errorprone.annotations.concurrent"
+    ],
+    "com.google.guava:failureaccess": [
+      "com.google.common.util.concurrent.internal"
+    ],
+    "com.google.guava:guava": [
+      "com.google.common.annotations",
+      "com.google.common.base",
+      "com.google.common.base.internal",
+      "com.google.common.cache",
+      "com.google.common.collect",
+      "com.google.common.escape",
+      "com.google.common.eventbus",
+      "com.google.common.graph",
+      "com.google.common.hash",
+      "com.google.common.html",
+      "com.google.common.io",
+      "com.google.common.math",
+      "com.google.common.net",
+      "com.google.common.primitives",
+      "com.google.common.reflect",
+      "com.google.common.util.concurrent",
+      "com.google.common.xml",
+      "com.google.thirdparty.publicsuffix"
+    ],
+    "com.google.j2objc:j2objc-annotations": [
+      "com.google.j2objc.annotations"
+    ],
+    "org.jspecify:jspecify": [
+      "org.jspecify.annotations"
+    ]
+  },
+  "repositories": {
+    "https://repo1.maven.org/maven2/": [
+      "com.google.errorprone:error_prone_annotations",
+      "com.google.errorprone:error_prone_annotations:jar:sources",
+      "com.google.guava:failureaccess",
+      "com.google.guava:failureaccess:jar:sources",
+      "com.google.guava:guava",
+      "com.google.guava:guava:jar:sources",
+      "com.google.guava:listenablefuture",
+      "com.google.j2objc:j2objc-annotations",
+      "com.google.j2objc:j2objc-annotations:jar:sources",
+      "org.jspecify:jspecify",
+      "org.jspecify:jspecify:jar:sources"
+    ]
+  },
+  "services": {},
+  "skipped": [],
+  "version": "3"
+}
diff --git a/tests/custom_maven_install/maven_missing_source_jar_install.json b/tests/custom_maven_install/maven_missing_source_jar_install.json
new file mode 100644
index 0000000..2a4a92a
--- /dev/null
+++ b/tests/custom_maven_install/maven_missing_source_jar_install.json
@@ -0,0 +1,125 @@
+{
+  "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
+  "__INPUT_ARTIFACTS_HASH": {
+    "com.google.guava:guava": -1316430090,
+    "repositories": -1949687017
+  },
+  "__RESOLVED_ARTIFACTS_HASH": {
+    "com.google.errorprone:error_prone_annotations": 1014409728,
+    "com.google.errorprone:error_prone_annotations:jar:sources": 2059442212,
+    "com.google.guava:failureaccess": 1715931538,
+    "com.google.guava:failureaccess:jar:sources": 1303858893,
+    "com.google.guava:guava": -2046459454,
+    "com.google.guava:guava:jar:sources": 544801871,
+    "com.google.guava:listenablefuture": 1079558157,
+    "com.google.j2objc:j2objc-annotations": -404209759,
+    "com.google.j2objc:j2objc-annotations:jar:sources": 1852793670,
+    "org.jspecify:jspecify": 117231129,
+    "org.jspecify:jspecify:jar:sources": -2134060298
+  },
+  "artifacts": {
+    "com.google.errorprone:error_prone_annotations": {
+      "shasums": {
+        "jar": "77440e270b0bc9a249903c5a076c36a722c4886ca4f42675f2903a1c53ed61a5",
+        "sources": "7e117e0931cb2cb4226372af336189b49edb79969d120ec958a6df0beacb0612"
+      },
+      "version": "2.36.0"
+    },
+    "com.google.guava:failureaccess": {
+      "shasums": {
+        "jar": "cbfc3906b19b8f55dd7cfd6dfe0aa4532e834250d7f080bd8d211a3e246b59cb",
+        "sources": "6fef4dfd2eb9f961655f2a3c4ea87c023618d9fcbfb6b104c17862e5afe66b97"
+      },
+      "version": "1.0.3"
+    },
+    "com.google.guava:guava": {
+      "shasums": {
+        "jar": "f3d7f57f67fd622f4d468dfdd692b3a5e3909246c28017ac3263405f0fe617ed",
+        "sources": "9d3c6aad893daac9d4812eb9fa4c3f7956a9f2e472eb7df2fea0e467fed7e766"
+      },
+      "version": "33.4.8-jre"
+    },
+    "com.google.guava:listenablefuture": {
+      "shasums": {
+        "jar": "b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99"
+      },
+      "version": "9999.0-empty-to-avoid-conflict-with-guava"
+    },
+    "com.google.j2objc:j2objc-annotations": {
+      "shasums": {
+        "jar": "88241573467ddca44ffd4d74aa04c2bbfd11bf7c17e0c342c94c9de7a70a7c64",
+        "sources": "bd60019a0423c3a025ef6ab24fe0761f5f45ffb48a8cca74a01b678de1105d38"
+      },
+      "version": "3.0.0"
+    },
+    "org.jspecify:jspecify": {
+      "shasums": {
+        "jar": "1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab",
+        "sources": "adf0898191d55937fb3192ba971826f4f294292c4a960740f3c27310e7b70296"
+      },
+      "version": "1.0.0"
+    }
+  },
+  "dependencies": {
+    "com.google.guava:guava": [
+      "com.google.errorprone:error_prone_annotations",
+      "com.google.guava:failureaccess",
+      "com.google.guava:listenablefuture",
+      "com.google.j2objc:j2objc-annotations",
+      "org.jspecify:jspecify"
+    ]
+  },
+  "packages": {
+    "com.google.errorprone:error_prone_annotations": [
+      "com.google.errorprone.annotations",
+      "com.google.errorprone.annotations.concurrent"
+    ],
+    "com.google.guava:failureaccess": [
+      "com.google.common.util.concurrent.internal"
+    ],
+    "com.google.guava:guava": [
+      "com.google.common.annotations",
+      "com.google.common.base",
+      "com.google.common.base.internal",
+      "com.google.common.cache",
+      "com.google.common.collect",
+      "com.google.common.escape",
+      "com.google.common.eventbus",
+      "com.google.common.graph",
+      "com.google.common.hash",
+      "com.google.common.html",
+      "com.google.common.io",
+      "com.google.common.math",
+      "com.google.common.net",
+      "com.google.common.primitives",
+      "com.google.common.reflect",
+      "com.google.common.util.concurrent",
+      "com.google.common.xml",
+      "com.google.thirdparty.publicsuffix"
+    ],
+    "com.google.j2objc:j2objc-annotations": [
+      "com.google.j2objc.annotations"
+    ],
+    "org.jspecify:jspecify": [
+      "org.jspecify.annotations"
+    ]
+  },
+  "repositories": {
+    "https://repo1.maven.org/maven2/": [
+      "com.google.errorprone:error_prone_annotations",
+      "com.google.errorprone:error_prone_annotations:jar:sources",
+      "com.google.guava:failureaccess",
+      "com.google.guava:failureaccess:jar:sources",
+      "com.google.guava:guava",
+      "com.google.guava:guava:jar:sources",
+      "com.google.guava:listenablefuture",
+      "com.google.j2objc:j2objc-annotations",
+      "com.google.j2objc:j2objc-annotations:jar:sources",
+      "org.jspecify:jspecify",
+      "org.jspecify:jspecify:jar:sources"
+    ]
+  },
+  "services": {},
+  "skipped": [],
+  "version": "3"
+}
diff --git a/tests/unit/build_tests/BUILD b/tests/unit/build_tests/BUILD
index b7298db..34a07fc 100644
--- a/tests/unit/build_tests/BUILD
+++ b/tests/unit/build_tests/BUILD
@@ -28,6 +28,27 @@
     ],
 )
 
+# https://github.com/bazel-contrib/rules_jvm_external/issues/1477
+# Guava (resolved with fetch_sources = True) depends on the empty listenablefuture
+# jar, which has no sources jar. Resolution should still succeed and the
+# source-less artifact should build. Exercised via both the Maven and Gradle
+# resolvers.
+build_test(
+    name = "maven_missing_source_jar",
+    targets = [
+        "@maven_missing_source_jar//:com_google_guava_guava",
+        "@maven_missing_source_jar//:com_google_guava_listenablefuture",
+    ],
+)
+
+build_test(
+    name = "gradle_missing_source_jar",
+    targets = [
+        "@gradle_missing_source_jar//:com_google_guava_guava",
+        "@gradle_missing_source_jar//:com_google_guava_listenablefuture",
+    ],
+)
+
 # Note: skylib's `native_test` would fail on "Exec format error" under the other platform, hence the following 2 steps
 [native_binary(
     name = "src_that_refers_to_a_valid_executable_{}_target".format(platform),