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()
}
+19 -21
View File
@@ -62,17 +62,6 @@ function detectOsType(): OsType {
return "linux";
}
/**
* A response body path is an opaque handle the backend minted, and the bridge
* writes them as `<data dir>/responses/<response id>`. Taking the last segment
* turns it back into the id the `/responses/{id}/body` route wants, which keeps
* the server from ever being asked for a path chosen by the page.
*/
function responseIdFromBodyPath(path: string): string {
const segments = path.split(/[/\\]/);
return segments[segments.length - 1] ?? path;
}
/**
* Answer the Tauri host-plugin commands, which ride outside the RPC envelope.
*
@@ -221,25 +210,34 @@ export function createBridgePlatform(baseUrl: string, token: string | null): Pla
},
files: {
async readFile(path) {
const res = await connection.fetch(`/responses/${responseIdFromBodyPath(path)}/body`);
readDir: async () => {
throw unsupported("Browsing the filesystem");
},
// No filesystem here, so a path is just a string this host echoes back.
url: (path) => path,
basename: async (path) => path.split(/[/\\]/).pop() ?? path,
resolveResource: async (path) => path,
},
// Bodies live on the bridge's disk and are addressed by response id. The
// server resolves the id through its database, so a page can only ever
// reach a body the engine actually wrote.
blobs: {
async read(id) {
const res = await connection.fetch(`/responses/${encodeURIComponent(id)}/body`);
if (res.status === 404) return null;
if (!res.ok) {
throw new Error(`Failed to read response body (${res.status})`);
}
return new Uint8Array(await res.arrayBuffer());
},
readDir: async () => {
throw unsupported("Browsing the filesystem");
},
// The `<img src>`/`<video src>` equivalent of Tauri's `convertFileSrc`.
// The token rides in the query because the browser makes these requests
// itself and the page cannot add a header to them.
url: (path) => connection.url(`/responses/${responseIdFromBodyPath(path)}/body`),
basename: async (path) => path.split(/[/\\]/).pop() ?? path,
resolveResource: async (path) => path,
async url(id) {
return connection.url(`/responses/${encodeURIComponent(id)}/body`);
},
},
rpc: <T,>(cmd: string, payload?: RpcPayload): Promise<T> => {