(
&self,
args: serde_json::Value,
ctx: ToolContext,
)
| 71 | } |
| 72 | |
| 73 | async fn execute( |
| 74 | &self, |
| 75 | args: serde_json::Value, |
| 76 | ctx: ToolContext, |
| 77 | ) -> Result<ToolResult, ToolError> { |
| 78 | let input: WebFetchInput = |
| 79 | serde_json::from_value(args).map_err(|e| ToolError::InvalidArguments(e.to_string()))?; |
| 80 | |
| 81 | let url = input.url.clone(); |
| 82 | |
| 83 | if !url.starts_with("http://") && !url.starts_with("https://") { |
| 84 | return Err(ToolError::InvalidArguments( |
| 85 | "URL must start with http:// or https://".to_string(), |
| 86 | )); |
| 87 | } |
| 88 | |
| 89 | ctx.ask_permission( |
| 90 | crate::PermissionRequest::new("webfetch") |
| 91 | .with_pattern(&url) |
| 92 | .always_allow(), |
| 93 | ) |
| 94 | .await?; |
| 95 | |
| 96 | let timeout_secs = input |
| 97 | .timeout |
| 98 | .unwrap_or(DEFAULT_TIMEOUT_SECS) |
| 99 | .min(MAX_TIMEOUT_SECS); |
| 100 | |
| 101 | let accept_header = match input.format.as_str() { |
| 102 | "markdown" => "text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1", |
| 103 | "text" => "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1", |
| 104 | "html" => "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1", |
| 105 | _ => "*/*", |
| 106 | }; |
| 107 | |
| 108 | let response = tokio::select! { |
| 109 | result = self.fetch_with_retry(&url, accept_header, timeout_secs) => result, |
| 110 | _ = tokio::time::sleep(std::time::Duration::from_secs(timeout_secs)) => { |
| 111 | return Err(ToolError::Timeout(format!("Request timed out after {} seconds", timeout_secs))); |
| 112 | } |
| 113 | _ = ctx.abort.cancelled() => { |
| 114 | return Err(ToolError::Cancelled); |
| 115 | } |
| 116 | }; |
| 117 | |
| 118 | let response = response?; |
| 119 | |
| 120 | let content_type = response |
| 121 | .headers() |
| 122 | .get("content-type") |
| 123 | .and_then(|v| v.to_str().ok()) |
| 124 | .unwrap_or("") |
| 125 | .to_string(); |
| 126 | |
| 127 | let content_length = response |
| 128 | .headers() |
| 129 | .get("content-length") |
| 130 | .and_then(|v| v.to_str().ok()) |
nothing calls this directly
no test coverage detected