crawl

package
v1.801.261 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package crawl is Hanzo Crawl: fetch one URL and return its readable content as markdown, in-process, in Go.

It replaces the dial to a separate Crawl4AI deployment. That service was named in config but did not exist — crawl.hanzo.svc.cluster.local was NXDOMAIN — so every scrape returned {success:false, "no such host"} while the surface in front of it answered 200. This package is the same move already made for the SEARCH half in clients/websearch/search.go, which replaced a SearXNG pod with in-process Go: one fewer non-Go dependency, one fewer thing that can be down, and no network hop for work that is a fetch and a parse.

The engine is three orthogonal steps, each in its own file and each testable without the others:

Fetch    (this file)  URL   → HTML bytes, over guarded HTTP
extract  (extract.go) HTML  → the readable subtree + metadata
render   (markdown.go) node → markdown

Fetching untrusted URLs from inside the cluster is the security boundary

The caller supplies the URL, so this is a server-side request forgery primitive by construction, and it runs INSIDE the cluster with an in-namespace service DNS and a cloud metadata endpoint on 169.254.169.254 that hands out credentials to anyone who asks. A naive fetcher here is a credential exfiltration hole, not a bug. The separate service got network policy for free by being a separate pod; folding it in means this package must carry the guard itself.

So: only http/https, and every address actually dialed must be a public unicast address. The check is in the DIALER, not on the hostname, because checking a hostname up front and then letting the transport resolve it again is a time-of-check/time-of-use gap — DNS rebinding walks straight through it. Dialing is the only moment the real destination is known, and that is where it is enforced. Redirects are followed but re-enter the same dialer, so a public URL that 302s to 169.254.169.254 is refused at the hop that matters.

Index

Constants

This section is empty.

Variables

View Source
var ErrBlocked = errors.New("crawl: refused to dial a non-public address")

ErrBlocked is returned when a URL resolves to an address this package refuses to dial. It is deliberately distinguishable: a caller may want to report a blocked fetch differently from a site that was merely down, and a log line that cannot tell them apart makes an attack look like an outage.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount registers /v1/crawl.

The gate mirrors /v1/websearch/search exactly, and it is not optional here. This surface fetches a URL the caller chooses, from inside the cluster — an open one is a proxy into the private network, and the address guard in crawl.go is the second line of that defence, not the first. A caller is admitted with EITHER a validated principal (a signed-in user, already authenticated and metered) OR the shared service key. Neither ⇒ refused. An unset key 503s rather than defaulting open, so a misconfigured deploy fails closed and loudly.

Types

type Document

type Document struct {
	URL      string         `json:"url"`
	Title    string         `json:"title,omitempty"`
	Markdown string         `json:"markdown"`
	Metadata map[string]any `json:"metadata,omitempty"`
}

Document is the crawled page.

type Page

type Page struct {
	// URL is the FINAL url after redirects, not the one requested. Callers cite it,
	// so it must be where the content actually came from.
	URL string
	// Title is the document title, best-effort from <title> or og:title.
	Title string
	// Markdown is the readable content. Empty is a legitimate result for a page
	// that is genuinely all chrome; it is not an error.
	Markdown string
	// Metadata carries what the document said about itself (description, og:*,
	// language, status). Map-shaped because it is passed through to a JSON contract
	// that predates this package and accepts arbitrary keys.
	Metadata map[string]any
}

Page is one crawled document. It is what every surface in front of this package renders, so it holds the extracted content and the provenance needed to cite it — not the raw HTML, which no caller has wanted and which would keep the whole response alive in memory.

func Fetch

func Fetch(ctx context.Context, raw string) (*Page, error)

Fetch retrieves one URL and returns its readable content.

It returns an error only when there is nothing to read — a refused address, an unreachable host, a non-2xx status, a body that is not a document. A page that is reachable but yields little content is a Page with short Markdown, because "this page is mostly navigation" is a true answer and a caller that treats it as a failure would retry forever.

type Request

type Request struct {
	URL string `json:"url"`
}

Request is the /v1/crawl body. One URL per call: batching would make the response a partial-failure envelope that every caller then has to unpack, and no caller has asked for more than one.

type Response

type Response struct {
	Success bool      `json:"success"`
	Data    *Document `json:"data,omitempty"`
	Error   string    `json:"error,omitempty"`
}

Response is the /v1/crawl body.

Success is a field rather than an HTTP status because "the page could not be fetched" is a normal outcome of asking about a URL, not a fault of the request: the caller sent a well-formed ask and gets a well-formed answer saying the page was unreachable. Reserving non-2xx for auth and malformed input keeps a caller's error handling honest — a 200 means the surface worked, and Success says what it found.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL