Summary
SyncEndpointUrl accepts HTTP endpoints that can later panic in websocket() or lose information when formatted and reparsed.
Update after review: the original IPv6 case was incorrect. With url 2.5.8, Url::host_str() already returns bracketed IPv6 hosts, so that case should be ignored. The remaining issue is the runtime panic plus several Display / round-trip cases.
Cases
Maximum explicit HTTP port panics when the WebSocket URL is derived
let endpoint: SyncEndpointUrl = "http://localhost:65535".parse()?;
let _ = endpoint.websocket();
Parsing succeeds, but deriving the WebSocket URL executes:
http_port.checked_add(1).expect("port overflow")
That is an unconditional panic in every build profile. Invalid derived-port combinations should be rejected during parsing instead of failing later from a public accessor. An explicit override should still be accepted, e.g. http://localhost:65535,ws=9000.
HTTP path, query, and fragment are lost by Display
let endpoint: SyncEndpointUrl =
"https://rpc.example.com/api/v1?key=value,wss=ws.example.com/websocket".parse()?;
let reparsed: SyncEndpointUrl = endpoint.to_string().parse()?;
assert_eq!(endpoint, reparsed);
Display reconstructs the HTTP side from only scheme, host, and port, so URL components such as /api/v1, ?key=value, and fragments are discarded.
HTTP userinfo is accepted by FromStr but dropped by Display
let endpoint: SyncEndpointUrl = "http://user:pass@localhost:8545,ws=8546".parse()?;
let reparsed: SyncEndpointUrl = endpoint.to_string().parse()?;
assert_eq!(endpoint, reparsed);
The parser accepts credentials because only the scheme is validated, but Display never emits them. Dropping credentials may be desirable for logs, but then Display should be treated as a log/debug format rather than a round-trippable serialization format.
Derived-vs-explicit WebSocket state breaks structural round-tripping
let endpoint: SyncEndpointUrl = "http://localhost:8545".parse()?; // ws: None
let reparsed: SyncEndpointUrl = endpoint.to_string().parse()?; // ws: Some(ws://localhost:8546/)
assert_eq!(endpoint, reparsed);
The two values resolve to the same websocket() URL, but they are structurally unequal because SyncEndpointUrl derives PartialEq over ws: Option<Url>. This affects the common/default form, including both default follow endpoints in crates/malachite-app/src/config.rs:
https://rpc.testnet.arc.io/
http://localhost:8545
Expected behavior
- Accepted endpoint values should not panic when
websocket() is called.
parse -> Display -> parse should preserve parser-produced endpoint values, or Display should be documented/treated as a lossy log format.
- Derived and explicitly equivalent WebSocket URLs should not become an accidental equality trap unless the distinction is intentionally meaningful.
Implementation notes
The cases split into two independent fixes.
1. Eagerly derive the WebSocket URL during parsing
Store ws: Url instead of ws: Option<Url>, deriving the default WebSocket URL inside FromStr when no override is supplied.
That would close:
- the
:65535 panic, because checked_add can fail during parsing and return Err
- the derived-vs-explicit round-trip failure, because both forms store the same concrete
Url
This does not require a Display change and should preserve the current canonical output for derived endpoints such as http://localhost:8545,ws=8546 and default-port forms such as https://example.com:443,wss=443.
The semantic caveat is that http://x:8545 and http://x:8545,ws=8546 would compare equal after parsing. That appears to have no production blast radius:
SyncEndpointUrl derives Debug, Clone, PartialEq, and Eq, but not Hash, so it cannot be used directly as a HashMap / HashSet key.
- I did not find any
dedup, retain, position, or contains use over rpc_sync_endpoints or PeerRegistry::endpoints.
PeerRegistry builds peers by iterating the endpoint list positionally rather than comparing entries.
So the equality change is visible to tests or future code that chooses to assert on it, but there does not appear to be a current production consumer of the derived-vs-explicit distinction.
2. Decide whether Display is serialization or logging
Path/query/fragment loss and userinfo loss are Display-format problems. Emitting the HTTP side with URL-aware serialization, e.g. self.http.as_str(), would preserve those components, but it also changes pinned output such as explicit default ports and raises the delimiter question.
The current format is comma-delimited via split_once(','). It round-trips only for values produced by the current parser shape. If future code adds a public constructor, serde support, or a builder that can produce URLs containing commas or =, Display-as-serialization would need proper escaping or a different format.
So this part should be a deliberate decision:
- If
Display is intended as a serialization format, use URL-aware serialization and handle delimiter/credential edge cases.
- If
Display is intended only for logs/debug output, document that it is lossy and avoid treating parse -> Display -> parse as a contract.
Summary
SyncEndpointUrlaccepts HTTP endpoints that can later panic inwebsocket()or lose information when formatted and reparsed.Update after review: the original IPv6 case was incorrect. With
url2.5.8,Url::host_str()already returns bracketed IPv6 hosts, so that case should be ignored. The remaining issue is the runtime panic plus severalDisplay/ round-trip cases.Cases
Maximum explicit HTTP port panics when the WebSocket URL is derived
Parsing succeeds, but deriving the WebSocket URL executes:
That is an unconditional panic in every build profile. Invalid derived-port combinations should be rejected during parsing instead of failing later from a public accessor. An explicit override should still be accepted, e.g.
http://localhost:65535,ws=9000.HTTP path, query, and fragment are lost by Display
Displayreconstructs the HTTP side from only scheme, host, and port, so URL components such as/api/v1,?key=value, and fragments are discarded.HTTP userinfo is accepted by FromStr but dropped by Display
The parser accepts credentials because only the scheme is validated, but
Displaynever emits them. Dropping credentials may be desirable for logs, but thenDisplayshould be treated as a log/debug format rather than a round-trippable serialization format.Derived-vs-explicit WebSocket state breaks structural round-tripping
The two values resolve to the same
websocket()URL, but they are structurally unequal becauseSyncEndpointUrlderivesPartialEqoverws: Option<Url>. This affects the common/default form, including both default follow endpoints incrates/malachite-app/src/config.rs:https://rpc.testnet.arc.io/http://localhost:8545Expected behavior
websocket()is called.parse -> Display -> parseshould preserve parser-produced endpoint values, orDisplayshould be documented/treated as a lossy log format.Implementation notes
The cases split into two independent fixes.
1. Eagerly derive the WebSocket URL during parsing
Store
ws: Urlinstead ofws: Option<Url>, deriving the default WebSocket URL insideFromStrwhen no override is supplied.That would close:
:65535panic, becausechecked_addcan fail during parsing and returnErrUrlThis does not require a
Displaychange and should preserve the current canonical output for derived endpoints such ashttp://localhost:8545,ws=8546and default-port forms such ashttps://example.com:443,wss=443.The semantic caveat is that
http://x:8545andhttp://x:8545,ws=8546would compare equal after parsing. That appears to have no production blast radius:SyncEndpointUrlderivesDebug,Clone,PartialEq, andEq, but notHash, so it cannot be used directly as aHashMap/HashSetkey.dedup,retain,position, orcontainsuse overrpc_sync_endpointsorPeerRegistry::endpoints.PeerRegistrybuilds peers by iterating the endpoint list positionally rather than comparing entries.So the equality change is visible to tests or future code that chooses to assert on it, but there does not appear to be a current production consumer of the derived-vs-explicit distinction.
2. Decide whether Display is serialization or logging
Path/query/fragment loss and userinfo loss are Display-format problems. Emitting the HTTP side with URL-aware serialization, e.g.
self.http.as_str(), would preserve those components, but it also changes pinned output such as explicit default ports and raises the delimiter question.The current format is comma-delimited via
split_once(','). It round-trips only for values produced by the current parser shape. If future code adds a public constructor, serde support, or a builder that can produce URLs containing commas or=, Display-as-serialization would need proper escaping or a different format.So this part should be a deliberate decision:
Displayis intended as a serialization format, use URL-aware serialization and handle delimiter/credential edge cases.Displayis intended only for logs/debug output, document that it is lossy and avoid treatingparse -> Display -> parseas a contract.