diff --git a/src/cargo/sources/registry/http_remote.rs b/src/cargo/sources/registry/http_remote.rs
index 326c3f34855..18734ef6ba0 100644
--- a/src/cargo/sources/registry/http_remote.rs
+++ b/src/cargo/sources/registry/http_remote.rs
@@ -126,16 +126,25 @@ struct CompletedDownload {
 }
 
 impl<'cfg> HttpRegistry<'cfg> {
-    pub fn new(source_id: SourceId, config: &'cfg Config, name: &str) -> HttpRegistry<'cfg> {
-        let url = source_id
-            .url()
-            .to_string()
+    pub fn new(
+        source_id: SourceId,
+        config: &'cfg Config,
+        name: &str,
+    ) -> CargoResult<HttpRegistry<'cfg>> {
+        if !config.cli_unstable().http_registry {
+            anyhow::bail!("usage of HTTP-based registries requires `-Z http-registry`");
+        }
+        let url = source_id.url().as_str();
+        // Ensure the url ends with a slash so we can concatenate paths.
+        if !url.ends_with('/') {
+            anyhow::bail!("registry url must end in a slash `/`: {url}")
+        }
+        let url = url
             .trim_start_matches("sparse+")
-            .trim_end_matches('/')
             .into_url()
             .expect("a url with the protocol stripped should still be valid");
 
-        HttpRegistry {
+        Ok(HttpRegistry {
             index_path: config.registry_index_path().join(name),
             cache_path: config.registry_cache_path().join(name),
             source_id,
@@ -149,7 +158,7 @@ impl<'cfg> HttpRegistry<'cfg> {
                 pending_ids: HashMap::new(),
                 results: HashMap::new(),
                 progress: RefCell::new(Some(Progress::with_style(
-                    "Fetching",
+                    "Fetch",
                     ProgressStyle::Ratio,
                     config,
                 ))),
@@ -159,7 +168,7 @@ impl<'cfg> HttpRegistry<'cfg> {
             requested_update: false,
             fetch_started: false,
             registry_config: None,
-        }
+        })
     }
 
     fn handle_http_header(buf: &[u8]) -> Option<(&str, &str)> {
@@ -245,7 +254,8 @@ impl<'cfg> HttpRegistry<'cfg> {
     }
 
     fn full_url(&self, path: &Path) -> String {
-        format!("{}/{}", self.url, path.display())
+        // self.url always ends with a slash.
+        format!("{}{}", self.url, path.display())
     }
 
     fn is_fresh(&self, path: &Path) -> bool {
diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs
index fc9c29510c1..95510e313b6 100644
--- a/src/cargo/sources/registry/mod.rs
+++ b/src/cargo/sources/registry/mod.rs
@@ -546,10 +546,7 @@ impl<'cfg> RegistrySource<'cfg> {
     ) -> CargoResult<RegistrySource<'cfg>> {
         let name = short_name(source_id);
         let ops = if source_id.url().scheme().starts_with("sparse+") {
-            if !config.cli_unstable().http_registry {
-                anyhow::bail!("Usage of HTTP-based registries requires `-Z http-registry`");
-            }
-            Box::new(http_remote::HttpRegistry::new(source_id, config, &name)) as Box<_>
+            Box::new(http_remote::HttpRegistry::new(source_id, config, &name)?) as Box<_>
         } else {
             Box::new(remote::RemoteRegistry::new(source_id, config, &name)) as Box<_>
         };
diff --git a/tests/testsuite/registry.rs b/tests/testsuite/registry.rs
index f9afa2919c6..7c4e3f6e07c 100644
--- a/tests/testsuite/registry.rs
+++ b/tests/testsuite/registry.rs
@@ -41,7 +41,7 @@ fn configure_source_replacement_for_http(addr: &str) {
             replace-with = 'dummy-registry'
 
             [source.dummy-registry]
-            registry = 'sparse+http://{}'
+            registry = 'sparse+http://{}/'
         ",
             addr
         )
@@ -2680,6 +2680,15 @@ fn http_requires_z_flag() {
 
     p.cargo("build")
         .with_status(101)
-        .with_stderr_contains("  Usage of HTTP-based registries requires `-Z http-registry`")
+        .with_stderr_contains("  usage of HTTP-based registries requires `-Z http-registry`")
         .run();
 }
+
+#[cargo_test]
+fn http_requires_trailing_slash() {
+    cargo_process("-Z http-registry install bar --index sparse+https://index.crates.io")
+        .masquerade_as_nightly_cargo()
+        .with_status(101)
+        .with_stderr("[ERROR] registry url must end in a slash `/`: sparse+https://index.crates.io")
+        .run()
+}