···6677## public
8899-- `GET /stream`: subscribe to the event stream. query params: `cursor` (optional, start from a specific event ID).
99+- `GET /stream`: subscribe to the event stream. query params: `cursor` (optional, start from a specific event ID). slow consumers may receive a `{"type":"error","error":"ConsumerTooSlow",...}` frame before the connection closes.
1010- `GET /stats`: get stats about the database (counts of repos, records, events; sizes of keyspaces on disk).
1111- `GET /health` / `GET /_health`: health check.
1212
+1-1
docs/xrpc/atproto.md
···1515- `com.atproto.sync.listRepos`
1616- `com.atproto.sync.getLatestCommit`
1717- `com.atproto.sync.requestCrawl` (adds the host to firehose sources in relay mode)
1818-- `com.atproto.sync.subscribeRepos` (WebSocket firehose stream, requires `relay` feature)
1818+- `com.atproto.sync.subscribeRepos` (WebSocket firehose stream, requires `relay` feature; slow consumers may receive a `ConsumerTooSlow` error frame before the connection closes)
+8-1
examples/statusphere.rs
···113113 .map(|h| h.to_string())
114114 .unwrap_or_else(|| did.to_string())
115115 };
116116- while let Some(event) = stream.next().await {
116116+ while let Some(item) = stream.next().await {
117117+ let event = match item {
118118+ Ok(event) => event,
119119+ Err(err) => {
120120+ tracing::warn!(err = %err, "hydrant stream closed");
121121+ break;
122122+ }
123123+ };
117124 if let Some(rec) = event.record {
118125 let did = rec.did.as_str().to_owned();
119126 match rec.action.as_str() {
+43-3
src/api/stream.rs
···2828}
29293030async fn handle_socket(mut socket: WebSocket, hydrant: Hydrant, query: StreamQuery) {
3131+ let send_timeout = hydrant.stream_send_timeout();
3132 let mut stream = hydrant.subscribe(query.cursor);
32333333- while let Some(evt) = stream.next().await {
3434+ while let Some(item) = stream.next().await {
3535+ let evt = match item {
3636+ Ok(evt) => evt,
3737+ Err(err) => {
3838+ let json = serde_json::json!({
3939+ "type": "error",
4040+ "error": err.code(),
4141+ "message": err.to_string(),
4242+ });
4343+ let _ = tokio::time::timeout(
4444+ send_timeout,
4545+ socket.send(Message::text(json.to_string())),
4646+ )
4747+ .await;
4848+ let _ =
4949+ tokio::time::timeout(std::time::Duration::from_secs(1), socket.close()).await;
5050+ break;
5151+ }
5252+ };
5353+3454 match serde_json::to_string(&evt) {
3555 Ok(json) => {
3636- if socket.send(Message::text(json)).await.is_err() {
3737- break;
5656+ match tokio::time::timeout(send_timeout, socket.send(Message::text(json))).await {
5757+ Ok(Ok(())) => {}
5858+ Ok(Err(_)) => break,
5959+ Err(_) => {
6060+ let err = serde_json::json!({
6161+ "type": "error",
6262+ "error": "ConsumerTooSlow",
6363+ "message": format!(
6464+ "stream socket send blocked for at least {} seconds",
6565+ send_timeout.as_secs()
6666+ ),
6767+ });
6868+ let _ = tokio::time::timeout(
6969+ std::time::Duration::from_secs(1),
7070+ socket.send(Message::text(err.to_string())),
7171+ )
7272+ .await;
7373+ let _ =
7474+ tokio::time::timeout(std::time::Duration::from_secs(1), socket.close())
7575+ .await;
7676+ break;
7777+ }
3878 }
3979 }
4080 Err(e) => {