fix: two root-cause bug fixes for web_search

Bug 1 — writer.shutdown() kills Node socket before response:
  Rust called shutdown() on the write half immediately after writing
  the command. This sent EOF to the Node.js daemon, whose safeWrite()
  checks socket.destroyed and silently bails. Fixed by dropping writer
  instead.

Bug 2 — daemon has zero macro support:
  The daemon's execute() never handles macro_name/query. Only url
  matters for the navigate action. Fixed search() to resolve all 12
  macros (@google_search, @youtube_search, etc.) to actual search URLs
  in Rust before sending navigate with cmd.url set.
This commit is contained in:
2026-06-13 20:00:20 +05:30
parent c213c3770b
commit 274020319f

View File

@@ -330,7 +330,11 @@ impl BrowserClient {
let (reader, mut writer) = stream.into_split(); let (reader, mut writer) = stream.into_split();
writer.write_all(format!("{}\n", payload).as_bytes()).await.map_err(BrowserError::Io)?; writer.write_all(format!("{}\n", payload).as_bytes()).await.map_err(BrowserError::Io)?;
writer.shutdown().await.map_err(BrowserError::Io)?; // Don't call writer.shutdown() — it sends EOF to the Node.js daemon,
// which causes the socket to close before the async response arrives.
// The daemon's safeWrite() checks socket.destroyed and bails silently.
// The socket pair (reader/writer) will be dropped after we read the response.
drop(writer);
let mut buf_reader = BufReader::new(reader); let mut buf_reader = BufReader::new(reader);
let mut line = String::new(); let mut line = String::new();
@@ -729,12 +733,58 @@ impl BrowserClient {
} }
} }
/// Search via macro. /// Search via macro — resolves macro names to actual search URLs
/// since the camoufox-browser daemon does not handle macros internally.
pub async fn search(&self, macro_name: &str, query: &str) -> BrowserResult<NavigationResult> { pub async fn search(&self, macro_name: &str, query: &str) -> BrowserResult<NavigationResult> {
let url = match macro_name {
"@google_search" | "google" => {
format!("https://www.google.com/search?q={}&hl=en", urlencoding(query))
}
"@youtube_search" | "youtube" => {
format!("https://www.youtube.com/results?search_query={}", urlencoding(query))
}
"@amazon_search" | "amazon" => {
format!("https://www.amazon.com/s?k={}", urlencoding(query))
}
"@reddit_search" | "reddit" => {
format!("https://www.reddit.com/search/?q={}", urlencoding(query))
}
"@wikipedia_search" | "wikipedia" => {
format!("https://en.wikipedia.org/w/index.php?search={}", urlencoding(query))
}
"@twitter_search" | "@x_search" | "twitter" | "x" => {
format!("https://x.com/search?q={}&src=typed_query", urlencoding(query))
}
"@yelp_search" | "yelp" => {
format!("https://www.yelp.com/search?find_desc={}", urlencoding(query))
}
"@spotify_search" | "spotify" => {
format!("https://open.spotify.com/search/{}", urlencoding(query))
}
"@linkedin_search" | "linkedin" => {
format!("https://www.linkedin.com/search/results/all/?keywords={}", urlencoding(query))
}
"@instagram_search" | "instagram" => {
format!("https://www.instagram.com/explore/search/keyword/?q={}", urlencoding(query))
}
"@tiktok_search" | "tiktok" => {
format!("https://www.tiktok.com/search?q={}", urlencoding(query))
}
"@twitch_search" | "twitch" => {
format!("https://www.twitch.tv/search?term={}", urlencoding(query))
}
// If it looks like a URL already, use it as-is
s if s.starts_with("http://") || s.starts_with("https://") => s.to_string(),
// Default: Google search
_ => {
warn!("Unknown macro '{}', falling back to Google search", macro_name);
format!("https://www.google.com/search?q={}&hl=en", urlencoding(query))
}
};
let resp = self.send_command("navigate", |cmd| { let resp = self.send_command("navigate", |cmd| {
cmd.macro_name = Some(macro_name.to_string()); cmd.url = Some(url);
cmd.query = Some(query.to_string()); cmd.wait = Some(5000);
cmd.wait = Some(3000);
}).await?; }).await?;
let data = self.check_response(resp)?; let data = self.check_response(resp)?;
Ok(NavigationResult { Ok(NavigationResult {
@@ -763,6 +813,24 @@ impl BrowserClient {
} }
} }
/// URL-encode a string for use in a search query.
fn urlencoding(input: &str) -> String {
// Simple URL encoding — replaces spaces with + and encodes special chars
let mut result = String::with_capacity(input.len());
for byte in input.bytes() {
match byte {
b' ' => result.push('+'),
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
result.push(byte as char);
}
_ => {
result.push_str(&format!("%{:02X}", byte));
}
}
}
result
}
impl Default for BrowserClient { impl Default for BrowserClient {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()