Skip to content

Commit 3eadb90

Browse files
committed
fix(link-preview): https-only previews and full-document parsing
- drop allowHttp: plain-http fetches would reach the URL validator's self-host loopback exception; previews are now explicitly https-only on both server (early null) and client (query never fires for http) - parse the full capped document instead of truncating at the first <body> substring, which could match inside head scripts/comments and drop metadata
1 parent e98cc9e commit 3eadb90

2 files changed

Lines changed: 12 additions & 8 deletions

File tree

  • apps/sim/app

apps/sim/app/api/link-preview/route.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@ const NEGATIVE_CACHE_TTL_SECONDS = 60 * 60
2626
const CACHE_KEY_PREFIX = 'link-preview:v1:'
2727

2828
/**
29-
* Parses preview metadata from the document head. Only the head matters for
30-
* previews, so the input is truncated at `<body>` before parsing; cheerio
31-
* handles attribute order, quoting, and entity decoding.
29+
* Parses preview metadata from the fetched document (already capped at
30+
* MAX_RESPONSE_BYTES); cheerio handles attribute order, quoting, and entity
31+
* decoding.
3232
*/
3333
function parsePreview(html: string): LinkPreview {
34-
const bodyIndex = html.search(/<body[\s>]/i)
35-
const $ = cheerio.load(bodyIndex === -1 ? html : html.slice(0, bodyIndex))
34+
const $ = cheerio.load(html)
3635

3736
const meta = (key: string): string | null => {
3837
const value = $(`meta[property="${key}"], meta[name="${key}"]`).first().attr('content')
@@ -54,7 +53,6 @@ function parsePreview(html: string): LinkPreview {
5453

5554
async function fetchPreview(url: string): Promise<LinkPreview> {
5655
const response = await secureFetchWithValidation(url, {
57-
allowHttp: true,
5856
timeout: FETCH_TIMEOUT_MS,
5957
maxRedirects: MAX_REDIRECTS,
6058
maxResponseBytes: MAX_RESPONSE_BYTES,
@@ -84,6 +82,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
8482
if (!parsed.success) return parsed.response
8583
const { url } = parsed.data.query
8684

85+
if (!url.startsWith('https://')) {
86+
return NextResponse.json({ preview: null })
87+
}
88+
8789
const redis = getRedisClient()
8890
const cacheKey = `${CACHE_KEY_PREFIX}${createHash('sha256').update(url).digest('hex')}`
8991
if (redis) {

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/external-link.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ interface ExternalLinkProps {
3333
* Favicon + quiet-underline external link with an OG-preview tooltip. The
3434
* preview query fires when the link renders, so metadata is normally cached
3535
* (client and server side) before the first hover; the tooltip shows the
36-
* destination URL until metadata arrives or when the site has none.
36+
* destination URL until metadata arrives or when the site has none. Previews
37+
* are https-only — plain-http links keep the URL tooltip, since fetching them
38+
* server-side would reach the URL validator's self-host loopback exception.
3739
*/
3840
export function ExternalLink({ href, hostname, children }: ExternalLinkProps) {
39-
const { data } = useLinkPreview(href)
41+
const { data } = useLinkPreview(href.startsWith('https://') ? href : undefined)
4042
const preview = data?.preview
4143

4244
return (

0 commit comments

Comments
 (0)