From 2c5c8343ef8c39cad0cb27cbc07bffc7141e345a Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Tue, 10 Nov 2026 21:15:13 -0800 Subject: [PATCH] fix(relay): bound invite preview host lookup (#9308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** #9306 (merged) added link-preview tags to the invite page. To build the media URLs, the relay looks up the community for the request host. This fixes a problem found in our review of #9306. **Problem:** The invite page waits for that lookup with no time limit. When the database is slow, the lookup can take as long as the pool acquire timeout (3 s) plus the statement timeout (120 s by default). During that time, a person who opens an invite link gets no page. Before #9306, this page did not use the database. **Fix:** The lookup gets 250 ms. If it does not finish, the relay sends the page without the preview tags. A healthy indexed host lookup finishes well inside that, so crawlers still get the tags, and nothing else changes: the media route, the router and the build are untouched. ### Validation - Added test: a host whose lookup never finishes gets the plain page. Without the timeout, this test hangs (confirmed locally; it was killed after 30 s). - The existing `invite_preview` and router invite tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow Co-authored-by: Tom Brow Co-authored-by: studio --- crates/buzz-relay/src/invite_preview.rs | 15 +++++++++++++-- previews/invite-video/README.md | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/buzz-relay/src/invite_preview.rs b/crates/buzz-relay/src/invite_preview.rs index e7c804051f..bcd29f3f15 100644 --- a/crates/buzz-relay/src/invite_preview.rs +++ b/crates/buzz-relay/src/invite_preview.rs @@ -4,9 +4,12 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse, Response}, }; -use std::path::Path; +use std::{path::Path, time::Duration}; use url::Url; +/// The lookup only serves link-preview metadata; never hold the page for it. +const HOST_LOOKUP_DEADLINE: Duration = Duration::from_millis(250); + pub(crate) async fn response( index: &Path, relay_url: &str, @@ -15,7 +18,13 @@ pub(crate) async fn response( ) -> Response { match tokio::fs::read_to_string(index).await { Ok(html) => { - let tenant = crate::tenant::bind_community(resolver, raw_host).await.ok(); + let tenant = tokio::time::timeout( + HOST_LOOKUP_DEADLINE, + crate::tenant::bind_community(resolver, raw_host), + ) + .await + .ok() + .and_then(Result::ok); Html(with_metadata(html, relay_url, tenant.as_ref())).into_response() } Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), @@ -90,6 +99,7 @@ mod tests { ))) } "error.example" => Err(()), + "slow.example" => std::future::pending().await, _ => Ok(None), } } @@ -111,6 +121,7 @@ mod tests { "beta.example:8443", "unknown.example", "error.example", + "slow.example", "", ] { let response = response(&index, relay, &Resolver, host).await; diff --git a/previews/invite-video/README.md b/previews/invite-video/README.md index 41a30ede07..01b6fbd7ea 100644 --- a/previews/invite-video/README.md +++ b/previews/invite-video/README.md @@ -43,7 +43,7 @@ do not validate or redeem the invite. Deploy both the updated relay and its web bundle (the relay Docker image includes both). The media host comes from the request's resolved community, matching the invite link's host. `RELAY_URL` supplies only the `wss`/`ws` TLS posture, mapped to -`https`/`http`. Unmapped hosts or lookup failures receive no preview metadata. +`https`/`http`. Unmapped hosts, lookup failures and slow lookups receive no preview metadata. Each community host must serve the web bundle without authentication. Use HTTPS in production.