Skip to content

Link preview rejects messages with bare-domain URLs due to trailing-slash normalization mismatch #6347

Description

@astromara

Describe the bug
Messages containing a bare-domain HTTPS URL (e.g., https://api.airtable.com) fail to send with invalid link-preview canonical URL. Desktop normalizes the URL by adding a trailing slash (https://api.airtable.com/), but the relay's validation requires the canonical URL to appear verbatim in the message body.

Steps to reproduce

  1. In Desktop, compose a message containing a bare-domain URL without a trailing slash, e.g.:
    Request failed for https://api.airtable.com returned code 403
    
  2. Click Send
  3. The button returns to its default state without sending; relay log shows status=400 reason="invalid: invalid link-preview canonical URL"

Expected behavior
The message should send. Either Desktop should preserve the original URL form (without normalizing), or the relay should normalize both sides before comparing.

Version and platform

  • Buzz version: 0.5.14
  • OS: macOS 26.5.2 (Apple Silicon)

Logs / additional context

Relay log:

{"timestamp":"2026-08-19T16:03:53.232211Z","level":"WARN","message":"HTTP bridge request","status":400,"accepted":false,"kind":9,"reason":"invalid: invalid link-preview canonical URL"}

Root cause:

JavaScript's URL class adds a trailing slash when normalizing bare-domain URLs:

new URL('https://api.airtable.com').href
// => "https://api.airtable.com/"

Desktop's linkPreview.ts:287 uses canonical.href (the normalized form) in the snapshot tag.

The relay's ingest.rs:259 then checks !event.content.contains(&parts[3]), which fails because the message body contains https://api.airtable.com but the tag contains https://api.airtable.com/.

Suggested fix options:

  1. Desktop-side: In createPreview(), preserve the original detected URL string for the canonical URL instead of round-tripping through new URL().href. The fragment-stripping can be done with string manipulation instead.

  2. Relay-side: Normalize the canonical URL before the contains check (strip trailing slash from root paths), or compare after URL parsing both sides.

Option 1 is probably cleaner since it makes Desktop's output match what the user typed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions