fix(relay): bound invite preview host lookup (#9308)

**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 <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
This commit is contained in:
Tom Brow
2026-11-10 21:15:13 -08:00
committed by GitHub
parent 47674dc322
commit 2c5c8343ef
2 changed files with 14 additions and 3 deletions

View File

@@ -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<R: crate::tenant::HostResolver>(
index: &Path,
relay_url: &str,
@@ -15,7 +18,13 @@ pub(crate) async fn response<R: crate::tenant::HostResolver>(
) -> 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;

View File

@@ -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.