nips icon indicating copy to clipboard operation
nips copied to clipboard

Recommend trailing slash for relay URLs in kind 10002

Open melvincarvalho opened this issue 9 months ago • 16 comments

In the Nostr ecosystem, relay URLs listed in kind 10002 events sometimes appear both with and without a trailing slash, such as:

wss://auth.nostr1.com  
wss://auth.nostr1.com/

This leads to duplication, inconsistencies in relay list handling, and unnecessary reconnections, since these are treated as distinct URLs by many clients.

According to RFC 3986 §6.2.3, URLs with and without a trailing slash are not considered equivalent. To promote consistency and interoperability, we suggest updating the specification for kind 10002 to include a note like:

"Relay URLs MUST include a trailing slash / to match the canonical form used by HTTP and WebSocket clients. Clients SHOULD normalize relay URLs by adding a slash if it is missing."

This change would help eliminate subtle bugs, relay duplication, and client-side fragmentation in relay management.


🔎 Examples of trailing-slash duplicates seen in the wild:

1. wss://auth.nostr1.com and wss://auth.nostr1.com/
2. wss://eden.nostr.land and wss://eden.nostr.land/
3. wss://nos.lol and wss://nos.lol/
4. wss://nostr-relay.app and wss://nostr-relay.app/
5. wss://nostr.bitcoiner.social and wss://nostr.bitcoiner.social/
6. wss://nostr.data.haus and wss://nostr.data.haus/
7. wss://nostr.fmt.wiz.biz and wss://nostr.fmt.wiz.biz/
8. wss://nostr.mom and wss://nostr.mom/
9. wss://nostr.oxtr.dev and wss://nostr.oxtr.dev/
10. wss://nostr.wine and wss://nostr.wine/
11. wss://offchain.pub and wss://offchain.pub/
12. wss://purplepag.es and wss://purplepag.es/
13. wss://pyramid.fiatjaf.com and wss://pyramid.fiatjaf.com/
14. wss://r.kojira.io and wss://r.kojira.io/
15. wss://relay-jp.nostr.wirednet.jp and wss://relay-jp.nostr.wirednet.jp/
16. wss://relay.0xchat.com and wss://relay.0xchat.com/
17. wss://relay.damus.io and wss://relay.damus.io/
18. wss://relay.mostr.pub and wss://relay.mostr.pub/
19. wss://relay.nostr.band and wss://relay.nostr.band/
20. wss://relay.nostr.bg and wss://relay.nostr.bg/
21. wss://relay.noswhere.com and wss://relay.noswhere.com/
22. wss://relay.primal.net and wss://relay.primal.net/
23. wss://relay.snort.social and wss://relay.snort.social/
24. wss://yabu.me and wss://yabu.me/

Standardizing this with some text would greatly help reduce inconsistencies and edge cases across Nostr clients and tooling.

melvincarvalho avatar Apr 13 '25 05:04 melvincarvalho

Using domains as relay identifiers has been one of the worst ideas on nostr. URL normalization is just the basics. In practice, many relays offer different uris (eg onion vs open web), multiple IPs for the same url (geo located caching) with different datasets, secure vs unsecure (ws vs wss), multiple domains for the same server with and without different datasets, etc etc etc.

Relays should have been pubkeys with a list of domains/IPs/separate options, that apps can choose to reach the data base on what protocols they have available (Tor or not).

We need to go back to identifying relays by their pubkey. The url is not a reliable identifier.

vitorpamplona avatar Apr 13 '25 15:04 vitorpamplona

We need to go back to identifying relays by their pubkey. The url is not a reliable identifier.

Well pubky does that

For now we're stuck with DNS for the most part. This issue is simply about just adding a trailing slash to prevent dups as canonical. A simple sentence of text would help in the NIP

melvincarvalho avatar Apr 13 '25 21:04 melvincarvalho

My code used to keep the two URLs separate due to the definition of URLs, but after that became a nightmare I changed into adding a trailing slash if it was missing and I changed the equality function so that the trailing slash would not affect equality comparisons.

I think we should mention in the NIP that URLs with an empty path must be interpreted as having the root "/" path.

mikedilger avatar Apr 14 '25 04:04 mikedilger

I think Clients should also do a basic check if it's a valid relay. There's a lot of people using something like damus.io instead of relay.damus.io, typos, invalid urls etc

greenart7c3 avatar Apr 14 '25 11:04 greenart7c3

⚠️ URL normalization is a minor, semantic hurdle when compared to the actual challenges associated with relay deduplication (Although I do agree and advocated for relay normalization )

There's a lot of people using something like damus.io instead of relay.damus.io, typos, invalid urls etc

It is way more than that. Almost every relay will accept a connection to any path. So both wss://relay.damus.io/ and wss://relay.damus.io/somestring/ will get a 200.

Image

If relays were encouraged to only use their root path, it would be easy to disqualify fuzzy strings, however, this is not realistic.

And since enforcing root path only is not realistic, cases like wss://myrelay.xyz/, wss://myrelay.xyz/inbox/ and wss://myrelay.xyz/outbox/ present challenge where in order to reliably deduplicate relays, deduplication needs to rely on relay fingerprinting. The easiest way to fingerprint a relay right now is NIP-11 (although unreliable), which then insinuates NIP-11 is involuntary. NIP-11 is voluntary, and should remain voluntary.

Clients should not have to concern themselves with insanely complex, stateful multi-stage relay deduplication logic in order to avoid simultaneous connections to a single relay

Two options IMO:

  1. Pubkeys - which opens a HUGE can of worms, bootstrapping fragility, seeding complexity/centralization and likely breaking changes
  2. Add a NIP-01 MUST that states that every relay should only have one path that accepts a connection, this fixes 99% of the issues for clients and passively filters out relay list garbage via disconnects. Examples:
    • [RelayA] myrelay.xyz/outbox/ is 200
    • [RelayA] myrelay.xyz/outbox/%20 is 404 (right now most relays accept the connection [200])
    • [RelayB] myrelay.xyz/ is 200
    • [RelayB, looks like RelayA] myrelay.xyz/outboxx [note the extra x] is 404 (right now most relays accept the connection [200])

❗❗❗ I strongly believe option 2 is the simplest solution that can be rolled out very easily by any relay operator, right now, even without any protocol changes and enables clients to deduplicate relays AND help users clean up relay lists AND improve UX; passively and without state

Yes, there are relays that use endpoints for data (nwc, filtering, membership), but TBH this shortcut is an anti-pattern for clients and can be established as an anti-pattern with a small change to NIP-01.

Without a change to NIP-01, relay operators can help clients alleviate implementation complexity right now by only accepting on a single path.

Trailing slash, it is a trivial task compared to the challenges listed above. Normalize client-side then dedup; done. Doesn't mean URLs shouldn't be normalized, but only scratches the surface of the problem.

Actual Malicious Case

The following cannot be programmatically deduplicated in an objective way without stateful, multi-stage processing (relay fingerprinting via NIP-11 and/or other means), because again, a single hostname could (and many do) have different relays on both / and /another-relay/. This case negatively affects any outbox implementation where a user with a list like this comments on a thread. As of today, every one of these endpoints is the same relay and all accept the connection. Image

dskvr avatar Apr 14 '25 11:04 dskvr

This is already in NIP 65: https://github.com/nostr-protocol/nips/pull/801

staab avatar Apr 14 '25 16:04 staab

This is already in NIP 65: #801

Thanks for linking NIP 65 (#801). While RFC 3986 treats trailing slashes as optional (equating wss://x.com and wss://x.com/), the lack of a canonical form in NIP 65 creates client-side inconsistencies. Trade-offs:

Trimmed (no slash):

  • Pros: Cleaner vanity, fewer bytes, aligns with some HTTP API conventions.
  • Cons: Ambiguity for pathless domains (is wss://x.com a base path or typo?).

Slash-included:

  • Pros: Explicitly denotes a path, matches WebSocket’s / default, avoids parsing edge cases.
  • Cons: Visually noisy, redundant if clients normalize anyway.

This isn’t a technical blocker, but inconsistent implementations will keep causing minor friction. Can we just pick one? I’m happy to draft a NIP clarification once a decision is reached.

melvincarvalho avatar Apr 14 '25 23:04 melvincarvalho

From the RFC:

In general, a URI that uses the generic syntax for authority with an empty path should be normalized to a path of "/".

But I don't mind clarifying, feel free to open a PR.

staab avatar Apr 14 '25 23:04 staab

An empty path is ambiguous. A specified "/" path is not. We should include the trailing slash.

mikedilger avatar Apr 15 '25 00:04 mikedilger

Because /path/ is not treated the same as /path I don't remove the trailing slash if it's there. But because domain.com/ is treated the same as domain.com I remove the trailing slash because it looks ugly.

fabianfabian avatar Apr 15 '25 06:04 fabianfabian

You should be adding the slash when there is no path

staab avatar Apr 15 '25 12:04 staab

Each client can normalize on their own, locally. We gain nothing by standardizing at the protocol level and it won't be possible anyway, the spec is too complicated and there will be clients doing the wrong thing always.

I for myself have no idea of what RFC-3986 means because it's a huge spec and who knows what it says about normalization in the context we're referring to it. I only know that because I tried to read it, unlike probably 99% of people.

In fact NIP-65 is already so immense no one reads it, so it's crazy to add more stuff to it and think that will do any good.

fiatjaf avatar Apr 15 '25 15:04 fiatjaf

We gain nothing by standardizing at the protocol level

NIPs generally aim for there to be one way of doing things, not multiple. That principle helps reduce ambiguity and edge cases across implementations. So it's worth forming consensus on what the canonical relay URL should look like.

Otherwise, inconsistencies persist — including redundant relay connections where one would suffice.

I for myself have no idea of what RFC-3986 means

It’s essentially the standard for how URLs work — foundational stuff that underpins most of the internet. It’s worth leaning on where possible.

For did-nostr, we’ll need to make a clear decision here. Consensus is far preferable to inventing a rule in isolation.

Put another way: what does nostr-tools do? That seems to be the closest thing to a reference implementation of nostr core right now.

melvincarvalho avatar Apr 15 '25 21:04 melvincarvalho

Please let's not call nostr-tools a "reference" implementation. We don't want to centralize in a specific codebase.

vitorpamplona avatar Apr 15 '25 22:04 vitorpamplona

Please let's not call nostr-tools a "reference" implementation. We don't want to centralize in a specific codebase.

OK, but then how do you do stringify, as different languages do it differently. You would need to copy nostr-tools for interop, no?

I'm going to go with what @staab and @mikedilger have said so far, as it seems to be rough consensus, and makes logical sense.

melvincarvalho avatar Apr 15 '25 22:04 melvincarvalho

Lots of overlapping conversation between this and #1198!

snarfed avatar Nov 09 '25 05:11 snarfed