Adapt the bridge to id-keyed response bodies

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-15 11:03:21 -07:00
co-authored by Claude Fable 5
parent e294e6bcef
commit 4122b9d72a
4 changed files with 90 additions and 44 deletions
+8 -9
View File
@@ -247,12 +247,12 @@ async fn response_body(
Query(_q): Query<BodyQuery>,
headers: HeaderMap,
) -> Response {
let response = match app.state.db().get_http_response(&id) {
Ok(response) => response,
let location = match app.state.locate_response_body(&id) {
Ok(location) => location,
Err(_) => return (StatusCode::NOT_FOUND, "No such response").into_response(),
};
let Some(body_path) = response.body_path else {
let Some(body_path) = location.path else {
return (StatusCode::NOT_FOUND, "Response has no body").into_response();
};
@@ -269,12 +269,11 @@ async fn response_body(
}
};
let content_type = response
.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case("content-type"))
.map(|h| h.value.clone())
.unwrap_or_else(|| "application/octet-stream".to_string());
let content_type = if location.content_type.is_empty() {
"application/octet-stream".to_string()
} else {
location.content_type
};
let range = headers.get(header::RANGE).and_then(|v| v.to_str().ok()).and_then(parse_range);
+33 -14
View File
@@ -490,27 +490,22 @@ async fn cmd_send_ephemeral_request(
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CmdHttpResponseBodyReq {
pub response: HttpResponse,
pub response_id: String,
pub filter: Option<String>,
}
/// The frontend hands back an id and never a path, so the only bodies reachable
/// here are ones the engine wrote and the database still knows about.
async fn cmd_http_response_body(
ctx: BridgeCtx,
req: CmdHttpResponseBodyReq,
) -> Result<FilterResponse> {
let Some(body_path) = req.response.body_path else {
let location = ctx.state.locate_response_body(&req.response_id).map_err(err)?;
let Some(body_path) = location.path else {
return Ok(FilterResponse { content: String::new(), error: None });
};
let content_type = req
.response
.headers
.iter()
.find_map(|h| {
if h.name.eq_ignore_ascii_case("content-type") { Some(h.value.as_str()) } else { None }
})
.unwrap_or_default();
let content_type = location.content_type.as_str();
let body = read_response_body(&body_path, content_type)
.await
.ok_or_else(|| RpcError { message: "Failed to find response body".to_string() })?;
@@ -525,6 +520,24 @@ async fn cmd_http_response_body(
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CmdHttpResponseBodyPathReq {
pub response_id: String,
}
/// The desktop host uses this to open the file itself. A tab cannot open a
/// path, so the bridge's browser host never calls it — it fetches
/// `/responses/:id/body` instead — but the command answers honestly for any
/// client that does, with the path on the bridge's machine.
async fn cmd_http_response_body_path(
ctx: BridgeCtx,
req: CmdHttpResponseBodyPathReq,
) -> Result<Option<String>> {
let location = ctx.state.locate_response_body(&req.response_id).map_err(err)?;
Ok(location.path.map(|p| p.to_string_lossy().to_string()))
}
/// Decode a response body from disk using the charset its Content-Type
/// declares. Ported from crates-tauri/yaak-app-client/src/encoding.rs.
async fn read_response_body(body_path: impl AsRef<Path>, content_type: &str) -> Option<String> {
@@ -576,16 +589,21 @@ async fn cmd_get_http_response_events(
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CmdGetSseEventsReq {
pub file_path: String,
pub response_id: String,
}
async fn cmd_get_sse_events(
_ctx: BridgeCtx,
ctx: BridgeCtx,
req: CmdGetSseEventsReq,
) -> Result<Vec<ServerSentEvent>> {
use eventsource_client::{EventParser, SSE};
let body = std::fs::read(&req.file_path).map_err(err)?;
let Some(body_path) = ctx.state.locate_response_body(&req.response_id).map_err(err)?.path
else {
return Ok(Vec::new());
};
let body = std::fs::read(&body_path).map_err(err)?;
let mut event_parser = EventParser::new();
event_parser.process_bytes(body).map_err(err)?;
@@ -1107,6 +1125,7 @@ rpc_commands! {
cmd_send_ephemeral_request,
cmd_http_response_body,
cmd_http_response_body_path,
cmd_http_request_body,
cmd_get_http_response_events,
cmd_get_sse_events,
+30
View File
@@ -84,6 +84,14 @@ impl BridgeCapabilities {
}
}
/// Where a response's body is, and what it is meant to be read as.
pub struct ResponseBodyLocation {
/// None when the response has no stored body.
pub path: Option<PathBuf>,
/// The response's declared `Content-Type`, empty when it has none.
pub content_type: String,
}
pub struct BridgeState {
data_dir: PathBuf,
query_manager: QueryManager,
@@ -199,6 +207,28 @@ impl BridgeState {
self.data_dir.join("responses")
}
/// Find a response's body from its id alone.
///
/// The tab hands back an id and never a path, so the only bodies reachable
/// through the bridge are ones the engine wrote and the database still
/// knows about. Every route and command that reads a body goes through
/// here for that reason.
pub fn locate_response_body(
&self,
response_id: &str,
) -> yaak_models::error::Result<ResponseBodyLocation> {
let response = self.db().get_http_response(response_id)?;
Ok(ResponseBodyLocation {
path: response.body_path.map(PathBuf::from),
content_type: response
.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case("content-type"))
.map(|h| h.value.clone())
.unwrap_or_default(),
})
}
pub fn db(&self) -> ClientDb<'_> {
self.query_manager.connect()
}