Skip to main content

hanzo_client/apis/
crawl_api.rs

1/*
2 * Hanzo Cloud API
3 *
4 * The Hanzo Cloud API as a customer calls it: every operation under /v1/ except the operator's admin product, relay routes, legacy spellings and capabilities still reached by flag. Tagged by product: the first path segment after /v1/.
5 *
6 * The version of the OpenAPI document: v1
7 * 
8 * Generated by: https://openapi-generator.tech
9 */
10
11
12use reqwest;
13use serde::{Deserialize, Serialize, de::Error as _};
14use crate::{apis::ResponseContent, models};
15use super::{Error, configuration, ContentType};
16
17
18/// struct for typed errors of method [`read_page`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum ReadPageError {
22    Status400(models::CrawlResult),
23    UnknownValue(serde_json::Value),
24}
25
26
27/// Reads one URL and answers with the page as markdown.  It fetches a single URL from inside the cluster and answers with the address it actually landed on, the document's title, its content rendered to MARKDOWN, and whatever the page said about itself. One URL per call: batching would make the answer a partial-failure envelope every caller then has to unpack.  A PAGE THAT COULD NOT BE FETCHED IS A NORMAL ANSWER, not a fault. An unreachable host, a refused address and a content type that is not a document all answer 200 with `success:false` and the reason in `error`, because the caller sent a well-formed ask and gets a well-formed answer. Non-2xx is reserved for a caller problem — 400 with the same body when there is no url, 401 for a bad key, 503 when the surface is unconfigured — so error handling can trust the status. Check `success` before reading `data`.  Admission is either a validated principal or the shared service key, presented as X-API-Key or a Bearer; neither is refused, and an unset key fails closed rather than opening the fetcher to the private network. Pages are archived under the scope of the VERIFIED principal and NEVER a scope named in the body, so a URL already read under that scope is answered from the archive without touching the network; a service caller has no org and its pages land in the shared corpus.  The URL is caller-supplied and dialled from INSIDE the cluster, which makes this a request-forgery primitive by construction. Only http and https are accepted, and every address actually dialled must be public unicast — loopback, link-local, private and multicast are refused. The check lives in the DIALER rather than on the hostname, because resolving a name to validate it and then letting the transport resolve it again is a gap DNS rebinding walks straight through; redirects re-enter the same dialer.
28pub async fn read_page(configuration: &configuration::Configuration, crawl_request: models::CrawlRequest) -> Result<models::CrawlResult, Error<ReadPageError>> {
29    // add a prefix to parameters to efficiently prevent name collisions
30    let p_crawl_request = crawl_request;
31
32    let uri_str = format!("{}/v1/crawl", configuration.base_path);
33    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
34
35    if let Some(ref user_agent) = configuration.user_agent {
36        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
37    }
38    if let Some(ref token) = configuration.bearer_access_token {
39        req_builder = req_builder.bearer_auth(token.to_owned());
40    };
41    req_builder = req_builder.json(&p_crawl_request);
42
43    let req = req_builder.build()?;
44    let resp = configuration.client.execute(req).await?;
45
46    let status = resp.status();
47    let content_type = resp
48        .headers()
49        .get("content-type")
50        .and_then(|v| v.to_str().ok())
51        .unwrap_or("application/octet-stream");
52    let content_type = super::ContentType::from(content_type);
53
54    if !status.is_client_error() && !status.is_server_error() {
55        let content = resp.text().await?;
56        match content_type {
57            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
58            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CrawlResult`"))),
59            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CrawlResult`")))),
60        }
61    } else {
62        let content = resp.text().await?;
63        let entity: Option<ReadPageError> = serde_json::from_str(&content).ok();
64        Err(Error::ResponseError(ResponseContent { status, content, entity }))
65    }
66}
67