pagewalk

package
v1.130.4 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package pagewalk is the page walk behind the api gateway's `paginate` block (issue #1535): the detection of a response's pagination signal, the address a walk moves from page to page, the follow step that reads the signal and moves it, the Retry-After pause, and the loop that merges every page's items into one sink. It knows nothing about how a page is requested or read: the gateway supplies a Requester, which is where the SSRF guards, credential injection, and byte caps live, and the walk drives it one page at a time.

Index

Constants

View Source
const (
	StoppedByEnd      = "end"
	StoppedByMaxPages = "max_pages"
	StoppedByMaxBytes = "max_bytes"
)

StoppedBy values. "end" is a page with no next signal or no items, "max_pages" the caller's page bound, and "max_bytes" the inline byte cap of api_invoke_endpoint (api_export fails past its cap instead, the all-or-nothing contract a partial asset would break).

Variables

View Source
var ErrPageDoesNotFit = errors.New("page does not fit under the byte cap")

ErrPageDoesNotFit is what a Sink returns when the page it was handed would take the merged result past the cap it holds. The walk stops with StoppedByMaxBytes and hands back the signal that led to the page, so the caller can resume from it.

Functions

func FinalCursor

func FinalCursor(lead *PaginationInfo) string

FinalCursor is the cursor or link that addressed the last page of a walk, for provenance. Empty on a one-page walk.

func ScalarString

func ScalarString(val any) string

ScalarString renders one JSON scalar as the text a wire format carries it in. The gateway uses it for query-string assembly and multipart field encoding, the walk for reading a page parameter back, so a number reaching the upstream reads the same whichever side of the request it travels on — notably float64, which the JSON decoder produces for every number and which %v would otherwise render in exponent form.

Types

type Address

type Address interface {
	// FollowURL moves the address to a next link. It refuses a link whose
	// scheme or host differs from the one the walk is pinned to.
	FollowURL(next string) error
	// Param reads a query parameter of the address ("" when unset).
	Param(name string) string
	// SetParam sets a query parameter of the address.
	SetParam(name, value string)
	// Target is the request the address currently points at.
	Target() Target
}

Address is where a walk requests its next page from. A proxied connection's address is the path and query joined to base_url; the built-in util connection's fetch_url address is the url inside the request body, because that is the document being paged. The walk is one implementation over both; only the place the next page is written to differs.

func NewAddress

func NewAddress(spec AddressSpec) (Address, error)

NewAddress picks the address for a walk. With WalkBodyURL set and a body carrying a url string, the walk moves that url; everything else is addressed by path and query.

type AddressSpec

type AddressSpec struct {
	BaseURL      string
	Path         string
	Query        map[string]any
	Body         any
	WalkBodyURL  bool
	ValidatePath func(string) error
}

AddressSpec is what NewAddress builds an Address from: the first page's request, the connection's base URL, whether the url in the body is the document walked (the util connection's fetch_url), and the path rule a followed link must satisfy (the gateway's validatePath).

type InlineMerge

type InlineMerge struct {
	Limit int64
	// Rendered measures the merged array as the caller's tool result
	// will render it. The limit is expressed against that rather than
	// against the compact bytes the pages hold, because what a client
	// accepts is the rendered result and the indentation between the two
	// is several times the compact size (issue #1606). nil measures the
	// compact bytes.
	Rendered func(items []json.RawMessage) int64
	// contains filtered or unexported fields
}

InlineMerge is api_invoke_endpoint's sink: the merged array held in memory under a byte limit. A page that would pass the limit is refused whole, so the result is always a prefix of the collection at a page boundary, and the walk reports where it stopped.

func (*InlineMerge) Add

func (m *InlineMerge) Add(items []json.RawMessage) error

Add is the Sink.

func (*InlineMerge) Merged

func (m *InlineMerge) Merged() []json.RawMessage

Merged returns the array to report. An empty walk is an empty array, not null: the caller asked for a collection.

func (*InlineMerge) Size added in v1.129.0

func (m *InlineMerge) Size() int64

Size is the bytes the merged array holds: what the call returns.

type Options

type Options struct {
	Paginate  PaginateInput
	Address   AddressSpec
	Requester Requester
	// Authorize is the route policy check run on every page's target.
	// nil when the deployment installed no policy.
	Authorize func(Target) error
	Sink      Sink
	// Now is the clock Retry-After dates are read against; nil is
	// time.Now.
	Now func() time.Time
}

Options is what New builds a Walk from.

type Page

type Page struct {
	Status      int
	Header      http.Header
	ContentType string
	Body        []byte
}

Page is one page as read: the status and headers the walk reports, the Content-Type, and the raw body.

type PaginateInput

type PaginateInput struct {
	// Items is the key of the array merged across pages ("data", "items",
	// "results", "value"), a dotted path to a nested one ("result.items"),
	// or "$" when the page body is the array. Required: guessing the key
	// is how a merged result silently becomes a list of envelopes.
	Items string `json:"items"`
	// CursorParam is the query parameter a body cursor (next_cursor,
	// nextPageToken, ...) is sent back as. A cursor signal on a page that
	// names no CursorParam and no PageParam fails the walk.
	CursorParam string `json:"cursor_param,omitempty"`
	// PageParam is the query parameter advanced when a page carries no
	// next signal (?page=N, ?offset=N). Its starting value must be present
	// in query_params; the first page is requested exactly as given.
	PageParam string `json:"page_param,omitempty"`
	// PageStep is what PageParam is advanced by per page. 1 (the default)
	// suits ?page=N; the page size suits ?offset=N.
	PageStep int `json:"page_step,omitempty"`
	// MaxPages bounds the walk. 0 means defaultMaxPages; the ceiling is
	// maxMaxPages. Reaching it is reported as stopped_by "max_pages", with
	// the signal for the next page in `pagination`.
	MaxPages int `json:"max_pages,omitempty"`
}

PaginateInput is the optional `paginate` block api_invoke_endpoint and api_export share. With it set, the gateway walks the pages of a collection inside the one tool call, merging the array Items names from every page. How the next page is reached is decided per page from the signal the response carries: a next URL is followed, pinned to the host the walk started on; a cursor is sent back as the query parameter CursorParam names; with neither, and PageParam named, that query parameter is advanced by PageStep until a page has no items.

type PaginationInfo

type PaginationInfo struct {
	HasMore    bool   `json:"has_more,omitempty"`
	NextCursor string `json:"next_cursor,omitempty"`
	NextURL    string `json:"next_url,omitempty"`
	Source     string `json:"source,omitempty"`
}

PaginationInfo is the structured pagination state api_invoke_endpoint surfaces to the model on every response. The model uses HasMore + NextCursor (or NextURL) to decide whether to issue a follow-up call. Without a `paginate` block the gateway does not follow the signal, so each loop iteration stays observable in the conversation and audit log; with one, Walk follows it inside the one call and the audit row carries the page count instead.

Fields are populated only when the upstream response carries a recognizable pagination signal. When none are populated the field is omitted from the JSON response — the model sees no pagination envelope and treats the response as terminal.

func Detect

func Detect(headers http.Header, body any) *PaginationInfo

Detect inspects a response's Link header and parsed JSON body for the common cursor patterns. Returns nil when no signal is found.

Detection order is significant: Link header (RFC 5988) is the authoritative pagination protocol and most-trustworthy when present, so it takes precedence over body-level cursor fields. Within the body, OData's `@odata.nextLink` is checked before the generic cursor names because OData responses commonly carry both (a `value` array AND a stray `next` field that means something else).

type Requester

type Requester interface {
	Do(ctx context.Context, target Target) (*http.Response, error)
	ReadPage(resp *http.Response) (Page, error)
}

Requester is what the gateway supplies to request and read a page. Do builds and sends the request for a Target, with every guard a single call gets; ReadPage buffers a 2xx answer under the caps a single call has, and fails any other status.

type Sink

type Sink func(items []json.RawMessage) error

Sink receives the items of one page in order. Items are the raw bytes of each array element, so what is merged is what the upstream sent: no re-encoding, no number rounding.

type Target

type Target struct {
	Path  string
	Query map[string]any
	Body  any
}

Target is where one page is requested from: the path and query joined to the connection's base URL, and the request body. An address writes all three on every page, holding the ones it does not move, so the gateway applies a Target without knowing which kind produced it.

type Walk

type Walk struct {

	// Stats is the page count, item count, and why the walk stopped.
	Stats WalkStats
	// Resume is the signal for the page after the last merged one when
	// the walk stopped early (max_pages or max_bytes); nil at the end.
	Resume *PaginationInfo
	// Lead is the signal that addressed the most recent page: the walk's
	// "final cursor" for provenance.
	Lead *PaginationInfo
	// Last is the last page read, whose status and headers the inline
	// output reports as a single call's would be.
	Last Page
	// contains filtered or unexported fields
}

Walk is the state of one walk. After Run, Stats, Resume, Lead and Last are what the caller reports.

func New

func New(opts Options) (*Walk, error)

New validates the paginate block against the request and binds the address the walk moves.

func (*Walk) Run

func (w *Walk) Run(ctx context.Context) error

Run walks the pages until the end, a bound, or a failure. A failure names the page: the walk's caller reports it whole, and a partial result is never returned as a success.

type WalkStats

type WalkStats struct {
	PagesFetched int    `json:"pages_fetched"`
	ItemsMerged  int    `json:"items_merged"`
	StoppedBy    string `json:"stopped_by"`
}

WalkStats is what a walk reports on the output of both tools, and what the audit row for the one call carries (the observability the per-call loop was keeping). The gateway embeds it as a pointer so a single-page call's output carries none of these fields rather than zeros.

Jump to

Keyboard shortcuts

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