f664e77c7e67c0342ba78b140386c4644d451a32
max
  Thu Sep 17 01:42:56 2026 -0700
hgHubConnect, lib: name the hub URL when a hub fails to load

#Preview2 week - bugs introduced now will need a build patch to fix
A hub that could not be opened showed up in the connected-hubs table as an
error message and nothing else: the name, description and assembly cells were
all empty, because they are only filled in from the trackHub struct that the
failed load never produced.  The URL was in the row only inside the javascript
of the Disconnect and Retry buttons, so there was no way to tell which hub the
error was about.  Print the URL in the name cell instead.

netParseUrl chops up its local copy of the URL as it parses, so none of its
errAborts could report what they were looking at.  Keep the caller's string
and name it in all of them.  A URL written with a single slash, https:/host/f,
then reported a non-numeric port, because with no :// the protocol defaults to
http and the host becomes "https" with an empty port - say what is actually
wrong with it.

Also tighten the encoding of the hub tables' output and of javascript string
literals, and use htmlEncode, not javaScriptLiteralEncode, for the option
labels in cgiMakeSelectDropList, which are HTML text.

diff --git src/lib/net.c src/lib/net.c
index 90da0944d83..1063a4d7f80 100644
--- src/lib/net.c
+++ src/lib/net.c
@@ -548,30 +548,34 @@
 }
 
 void netParseUrl(char *url, struct netParsedUrl *parsed)
 /* Parse a URL into components.   A full URL is made up as so:
  *   http://user:password@hostName:port/file;byterange=0-499
  * User and password may be cgi-encoded.
  * This is set up so that the http:// and the port are optional. 
  */
 {
 char *s, *t, *u, *v, *w, *br, *bl;
 char buf[MAXURLSIZE];
 
 /* Make local copy of URL. */
 if (strlen(url) >= sizeof(buf))
     errAbort("Url too long: '%s'", url);
+/* keep the caller's string for the error messages below: the local copy gets
+ * chopped into pieces as we parse, so by the time something goes wrong there is
+ * no url left to report */
+char *origUrl = url;
 strcpy(buf, url);
 url = buf;
 
 /* Find out protocol - default to http. */
 s = trimSpaces(url);
 s = stringIn("://", url);
 if (s == NULL)
     {
     strcpy(parsed->protocol, "http");
     s = url;
     }
 else
     {
     *s = 0;
     tolowers(url);
@@ -648,70 +652,77 @@
     cgiDecode(parsed->password,parsed->password,strlen(parsed->password));
     s = v+1;
     }
 
 
 // Whenever IPv6 address : port are provided,
 // the address MUST be surrounded by square brackets like [IPv6-address]:port
 // because without the square brackets, we cannot tell if the trailing bit
 // is end end of an IPv6 address, or port number.
 
 int blCount = countChars(s, '[');
 int brCount = countChars(s, ']');
 
 // double-check any stray brackets
 if ((brCount != blCount) || (brCount > 1))
-    errAbort("badly formed url, stray square brackets in IPv6 address");
+    errAbort("badly formed url %s, stray square brackets in IPv6 address", origUrl);
 
 /* Save port if it's there.  If not default to 80. */
 bl = strchr(s, '['); // IPV6 address in url surrounded by brackets []
 br = strrchr(s, ']'); // IPV6 address in url surrounded by brackets []
 
 if (!br != !bl)  // logical XOR
-    errAbort("badly formed url, unbalanced square brackets around IPv6 address.");
+    errAbort("badly formed url %s, unbalanced square brackets around IPv6 address.", origUrl);
 
 if (!br && isIpv6Address(s))  // host looks like IPv6 address but no brackets.
-    errAbort("badly formed url, should be protocol://[IPv6-address]:port/. Put square brackets around literal IPv6 address.");
+    errAbort("badly formed url %s, should be protocol://[IPv6-address]:port/. Put square brackets around literal IPv6 address.", origUrl);
 
 // trim off the brackets around the ipv6 host name
 if (br)
     {
     // expecting *s == [
     if (*s != '[')
-	errAbort("badly formed url %s, expected [ at start of ipv6 address", s);
+	errAbort("badly formed url %s, expected [ at start of ipv6 address", origUrl);
     ++s;    // skip [
     *br = 0; // erase ]
     t = br+1;
     char c = *t;
     if (c == 0)
 	t = NULL;
     else if (c != ':')
-	errAbort("badly formed url %s, stray characters after ] at end of ipv6 address", s);
+	errAbort("badly formed url %s, stray characters after ] at end of ipv6 address", origUrl);
     }
 else
     {
     t = strrchr(s, ':');
     }
 
 if (br && !isIpv6Address(s))  // host has brackets but does not look like IPv6 address.
-    errAbort("badly formed url, brackets found, but not valid literal IPv6 address.");
+    errAbort("badly formed url %s, brackets found, but not valid literal IPv6 address.", origUrl);
 
 if (t) // the port was explicitly provided
     {
     *t++ = 0;
     if (!isdigit(t[0]))
-	errAbort("Non-numeric port name %s", t);
+	{
+	/* a url with only one slash, https:/host/file, has no "://" so the protocol
+	 * defaults to http, the host becomes "https" and the port is empty.  Say what
+	 * is really wrong with it rather than complaining about the port. */
+	if (isEmpty(t) && !stringIn("://", origUrl))
+	    errAbort("badly formed url %s, expected :// after the protocol", origUrl);
+	errAbort("Non-numeric port name '%s' in url %s", t, origUrl);
+	}
     safecpy(parsed->port, sizeof(parsed->port), t);
     }
 else // get default port for each protocol
     {  
     if (sameWord(parsed->protocol,"http"))
 	strcpy(parsed->port, "80");
     if (sameWord(parsed->protocol,"https"))
 	strcpy(parsed->port, "443");
     if (sameWord(parsed->protocol,"ftp"))
 	strcpy(parsed->port, "21");
     }
 
 /* What's left is the host. */
 safecpy(parsed->host, sizeof(parsed->host), s);
 }