transport

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package transport is forgeapi's one request core: addressing, auth injection, retry, the SSRF hook, trust, decode bounds, pagination, budget, error mapping and the per-request log line.

It is under internal/ because its whole job is to hold the three dependency types the exported surface bans from a signature, so an importable spelling would put a dependency's next major on that surface. The path decides it mechanically: the surface generator skips any directory named internal.

A family package builds one Conn per connection and speaks to it in this package's own vocabulary. Nothing here reaches a consumer.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Call added in v1.1.0

func Call(ctx context.Context, op string) context.Context

Call opens one operation and returns the context its every request is issued on, which is what makes the published price a CALL's price rather than a request's.

A family calls it once per operation, because only the family knows where one begins: on this library's own hot path a list costs one request plus one per row, and nothing beneath the family can tell that from several calls of one operation. The boundary is therefore stated rather than inferred from the method name, which cannot tell a second call of one operation from a second request of the first.

A request issued outside a Call is priced as a call of its own, so an operation that forgets to open one under-reports rather than accumulating without bound.

func CallWrite added in v1.1.0

func CallWrite(ctx context.Context, op string) context.Context

CallWrite is Call for a mutating operation whose call also reads: every read it makes is part of its write, so a read-only client refuses it with forgeapi.CodeMutationsDisabled before anything is sent, and the mutation reserve, held back for a write and its follow-up reads, admits it.

func ContextSentinel

func ContextSentinel(err error) error

ContextSentinel re-exports the context-sentinel test for a package that reads a transport failure before choosing its own answer.

func Decode

func Decode(body []byte, v any) error

Decode reads one response body under the decode bounds, which is what keeps an untrusted response's cost bounded before allocation scales with it.

func IsDeferred

func IsDeferred(err error) bool

IsDeferred reports whether an error is the governor's deferral rather than an answer an instance gave, which is what lets a list report the rows it did not fetch as partial instead of failing the call.

func IsThrottled

func IsThrottled(err error) bool

IsThrottled reports whether an error is a throttle the INSTANCE answered rather than a deferral this library chose, which the status is what separates: a deferral sent nothing and so carries none. It is what lets a fold refused by a throttle mark its row with the reason the envelope assigns that refusal instead of failing the list around it.

func Local

func Local(code, message string) *forgeapi.Error

Local re-exports the local-refusal shape for a family's own pre-request checks.

func ParseOriginAndPath

func ParseOriginAndPath(raw string) (*url.URL, bool)

ParseOriginAndPath parses raw when it is an origin and a path, the one form a base URL takes, and answers false for any other form or a URL that does not parse.

func Sanitize

func Sanitize(s string) string

Sanitize bounds and rune-sanitizes untrusted upstream text for a package that carries it on an error or a log line without importing the sanitizer itself.

Types

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is one connection's request core: one wire transport, two clients over it, and its governor.

There are two clients because a transport configuration is consumed once per round tripper and carries no per-request override, so the read path and the mutation path cannot share one: the read path retries non-idempotent requests and the mutation path takes exactly one attempt, because a replayed merge is a duplicate no interval undoes.

func Open

func Open(conn *forgeapi.Connection, set *forgeapi.Settings, opts Options) (*Conn, error)

Open validates one connection and builds its request core. It performs no I/O: what it refuses is a client it could not run honestly, and each refusal carries its own code so a consumer names the remedy rather than showing a warning.

func OpenEndpoint

func OpenEndpoint(conn *forgeapi.Connection, set *forgeapi.Settings, family forgeapi.Family) (*Conn, error)

OpenEndpoint builds the request core for one connection's OAuth endpoints, which live under its web base, so its API root plays no part. It carries the connection's trust material, proxy, address policy, port allowlist and headers exactly as Open does, and no credential. Its one exchange is Conn.PostForm.

func (*Conn) APIBase

func (c *Conn) APIBase() *url.URL

APIBase is the connection's API root, the one the connection named or the one its family derived, which is where a document served BESIDE the REST routes lives.

func (*Conn) AdmitFold

func (c *Conn) AdmitFold(op string) (forgeapi.PartialReason, bool)

AdmitFold asks the governor whether one folded-status read may be issued inside the current rolling interval, and says why not when it may not.

func (*Conn) Budget

func (c *Conn) Budget() forgeapi.Budget

Budget is the resolved budget.

func (*Conn) BudgetState

func (c *Conn) BudgetState() forgeapi.BudgetState

BudgetState is the governor's state for this connection, read-only and never written from outside.

func (*Conn) Close

func (c *Conn) Close()

Close releases this connection's pool: the idle connections of the ssrf transport Open built, which the connection holds by its concrete type. A substitute wire is its author's, so Close never reaches it.

func (*Conn) Counters

func (c *Conn) Counters() forgeapi.Counters

Counters is the consumer's counter set.

func (*Conn) Do

func (c *Conn) Do(ctx context.Context, req *Request) (*Response, error)

Do issues one request, maps its outcome and records what it cost.

Every read it issues is UNCONDITIONAL, and no request carries a validator. A validator only pays where the sender can answer a not-modified response from something it holds, and nothing above this core holds a response body: the per-connection state is the minted merge handles and the budget, a decoded body kept beside them would be the repository cache the boundary rules out, and no published return has a member that says unchanged. So a not-modified answer has nowhere to go, and one arriving anyway is an unexpected status the mapper reports like any other rather than a success arm nothing can spend. If a measured saving ever justifies the exchange, its shape is an opt-in caching round tripper inside this connection's own chain, which remembers the body it would serve.

What the call spent is published on EVERY arm this function leaves by, the two refusals that send nothing included. The figure is the last CALL's price, so a call that reached no wire publishes the zero it spent: leaving the previous call's figure standing would report a cost for a call that had none.

func (*Conn) Fail

func (c *Conn) Fail(ctx context.Context, op string, ex Exchange, code string, kind forgeapi.ErrorKind, status int, message string) *forgeapi.Error

Fail builds a failure the family reached after a request: it names the operation and the family, so it carries a diagnostic id, and the line that id names is recorded here rather than left to the caller.

The exchange is the one the family reached this failure ON, which only the family knows: the response is already in its hand and this core never saw which of the two arms answered it.

func (*Conn) FailBody

func (c *Conn) FailBody(ctx context.Context, op string, ex Exchange, status int) *forgeapi.Error

FailBody is what a response body this library could not read answers: the instance replied, so the failure is upstream's shape rather than a local refusal, and it carries the response's OWN status. Fabricating a 200 there would contradict the published invariant that the status is the real one always, and it is immediately wrong on a creation, which answers 201.

The code is the validation one, which is the code a response this library will not accept already carries where a family meets one it can read: a consumer branches on that field, so leaving it empty would hand it nothing on exactly the failure it cannot otherwise classify.

func (*Conn) Logger

func (c *Conn) Logger() *slog.Logger

Logger is the injected logger, for the family's own lines.

func (*Conn) PageCall

func (c *Conn) PageCall(op string, repo forgeapi.RepoRef, set *forgeapi.ListSettings) PageCall

PageCall names the call one list operation is making on this connection. op is the operation as its role method spells it; repo is the repository the call addresses, the zero reference on a list no repository addresses. The repository and an owner fold as the derived repository identifier folds them, and the API base folds as [connectionOf] states, so two spellings of one call name one call.

func (*Conn) PostForm

func (c *Conn) PostForm(ctx context.Context, op, path string, form url.Values) (*Response, error)

PostForm sends one form-encoded POST to path under the web base and answers the response WHATEVER its status: an OAuth endpoint answers its outcomes in the body, at 200 on one product and at 400 on another, so the caller reads the body.

A body that could not be read, the connection dropping mid-body or the body passing the endpoint's bound, answers the response's status and headers with no body beside the failure, because a token endpoint's success says the instance issued a token whether or not its body arrived. Every other failure answers no response.

It takes one attempt, because a token request that reached the instance and lost its answer has already spent the code or the refresh token it carried. It meets neither the mutation gate nor the mutation interval, which govern forge objects rather than the connection's own credential.

func (*Conn) Price

func (c *Conn) Price(cost int)

Price replaces what the governor reports as the last call's cost, for the one product that prices a call by a figure in its own RESPONSE rather than by the number of requests that left.

Conn.Do publishes the wire count on every arm it leaves by, so a family calling this AFTER Do has returned is stating the same call's cost in the units that product bills in. Measured: GitLab's GraphQL endpoint answers a complexity object beside the data, and the expectation table prices a document by that score and a REST request at one. A family with no such figure never calls this and the wire count stands.

func (*Conn) Refuse

func (c *Conn) Refuse(ctx context.Context, op string, capability forgeapi.Capability, support forgeapi.Support, ev forgeapi.Evidence) *forgeapi.Error

Refuse is the capability refusal: no request, one dedicated code, and the operation, the capability and the evidence detection holds for the verdict.

The message is worded from the SUPPORT VALUE rather than from the refusal, because the two verdicts it refuses on are different facts. A capability detection read as absent is one thing; one it could not establish, which an instance with its document switched off produces, is another, and asserting the first for the second tells a user their forge lacks a feature it may well have. The evidence says which, and now so does the message.

func (*Conn) Rotate

func (c *Conn) Rotate(cursor forgeapi.RotationCursor)

Rotate records how far the rotation of those reads got, so the next interval starts after the row last served rather than at the first.

func (*Conn) SpendFold

func (c *Conn) SpendFold(d time.Duration)

SpendFold charges one folded-status read's measured wall clock to the current interval, which is what the wall-clock bound spends.

func (*Conn) UnderAPI

func (c *Conn) UnderAPI(address string) (string, bool)

UnderAPI is the escaped path an absolute address names beneath this connection's API root, and whether it names one: another origin, compared as the redirect policy compares origins, or a path outside the root names nothing on this connection.

func (*Conn) WebBase

func (c *Conn) WebBase() *url.URL

WebBase is the connection's web base URL, which is where a document served at the instance root rather than under the API root lives.

type Exchange

type Exchange struct {
	// contains filtered or unexported fields
}

Exchange is what one exchange with an instance was: the method it used and the arm that carried it. The per-request record names both from here rather than assuming either, and a family that reaches a failure AFTER a response states the exchange it reached it on.

The ZERO value describes NO exchange, which is what a refusal reached before any request is. The record then carries neither attribute, because naming a method for an exchange that never happened is worse than naming none.

func Document

func Document(method string) Exchange

Document names one exchange on the GraphQL document arm, which is the two reads one product answers from a document and nothing else.

func REST

func REST(method string) Exchange

REST names one exchange on the REST arm, which is every route of every family.

type Options

type Options struct {
	Mapper        errorMapper
	Signal        budgetSignal
	DeriveAPIBase func(web *url.URL) string
	Clock         func() time.Time
	Family        forgeapi.Family
}

Options is what a family hands Open: the connection, the resolved settings, the family this client serves, the API base to use when the connection names none, and the two per-family functions this package cannot own.

Clock is the time source the governor's rolling interval and a reported reset are measured on, time.Now where it is nil. Only a test sets it, to move a bound no caller can advance; no exported surface carries it, so a consumer cannot replace the pacing this governor exists to do.

type PageCall

type PageCall struct {
	// contains filtered or unexported fields
}

PageCall is one list call as every continuation it mints names it: the family, the connection, the operation, the repository, the resolved state filter, the scope and any since bound, folded into a fixed-length digest, and the page bound. A continuation that names another call names no position in this one, so both resume methods refuse it before any request.

func (PageCall) Mint

func (p PageCall) Mint(prefix string, position ...int) forgeapi.Cursor

Mint is the page-numbered continuation resuming this call at position: prefix, then the position's numbers in order, the page bound and the call's digest, separated.

func (PageCall) MintPosition

func (p PageCall) MintPosition(prefix string, position ...string) forgeapi.Cursor

MintPosition is the continuation resuming this call at an upstream connection's own position: prefix, then the position's fields in order and the call's digest, separated. Each field is the family's own encoding and carries no separator. It carries no page bound, since a connection position resumes at the row after the last one served whatever page size the next call asks.

func (PageCall) Resume

func (p PageCall) Resume(c forgeapi.Cursor, prefix string, count int) ([]int, error)

Resume reads the count numbers of the position a continuation this call minted under prefix names. It refuses, with forgeapi.CodeCursorInvalid, another encoding, another call's continuation, and one minted at another page bound, whose message names the bound the walk began at, since that bound alone resumes it. Whether each number is one the family mints is the family's to judge.

func (PageCall) ResumePosition

func (p PageCall) ResumePosition(c forgeapi.Cursor, prefix string, count int) ([]string, error)

ResumePosition reads the count fields of the position a continuation this call minted with PageCall.MintPosition under prefix names, refusing another encoding and another call's continuation with forgeapi.CodeCursorInvalid. Whether each field is one the family mints is the family's to judge.

type Request

type Request struct {
	Body any
	// Headers are the FAMILY's own request headers for this request, written
	// after the credential and before the consumer's. One family needs them and
	// the reason is per request rather than per connection: its REST API-version
	// pin is sent only to an instance known to carry that version, and what
	// establishes that is a header riding a response the connection already
	// received, so the first request of a connection carries no pin and the rest
	// do. A name here is one the library writes, so it is on the reserved list and
	// a consumer's own entry can never collide with it.
	Headers []forgeapi.Header
	Query   url.Values
	Op      string
	Method  string
	Path    string
	// EscapedPath is this request's path ALREADY percent-encoded, taken in place
	// of Path where it is set. Exactly one family needs it, and the reason is
	// measured rather than stylistic: GitLab's repository selector is a full
	// namespace path its API takes as ONE path segment with the separators
	// encoded, and Path is assigned to [net/url.URL.Path], which is the DECODED
	// path. So a selector encoded into Path leaves as %252F and one left decoded
	// leaves as extra path segments, and gitlab.com answers 404 to both.
	EscapedPath string
	AbsoluteURL string
	// Answers are the non-2xx statuses this request reads as an ANSWER rather than
	// a refusal: [Conn.Do] returns such a response with its body and no error, and
	// the family decodes the outcome from it. One route needs it: GitHub's
	// asynchronous merge answers a merge already in flight with a 409 whose body
	// names that merge's handle, which is the outcome, not a conflict.
	Answers  []int
	MaxBytes int64
	// OwnerStatus is the status this request's route answers for an owner
	// [forgeapi.WithOwner] named and the instance does not resolve, zero on a
	// request no owner scopes. [Conn] maps exactly that status to
	// [forgeapi.CodeOwnerUnresolved] before the family's [Mapper] is asked,
	// because the cell is the scoped request's: the operation's default scope
	// names no owner, and the same status there is the ordinary cell.
	OwnerStatus int
	// Page is which page of a paged read this request asks for, so the
	// per-request record carries it. One where the read is not paged.
	Page int
	// Document says this request carries a GraphQL document rather than a REST
	// route, which is what the per-request record's transport attribute names.
	// One family in scope reaches an instance over two arms and degrades from
	// the document one to the other at runtime, so the record has to say which
	// arm a line describes: a constant there reports the refusal that degrades a
	// connection as a REST failure, which is the one line an operator reads to
	// learn why it degraded.
	//
	// A document request is a read whatever verb carries it, unless Mutation
	// says otherwise: see [Request.mutates].
	Document bool
	// Mutation says this document is a GraphQL mutation, which travels as the
	// same POST a query does and changes the instance, so only the document's
	// own kind tells the two apart.
	Mutation bool
}

Request is one call this package makes for a family.

type Response

type Response struct {
	Header http.Header

	Ended      string
	Refused    string
	Body       []byte
	Status     int
	Redirected bool
	// contains filtered or unexported fields
}

Response is what one call answered: the status, the headers, the decoded-ready bytes, and whether a redirect was followed to get them.

Redirected is the transport's record that this exchange took a hop, which is the rename witness this layer owns rather than a family: one product answers a renamed repository with a 301 the follow-and-revalidate policy would otherwise follow silently, and a FINAL-PATH comparison cannot be the witness, because the same product answers a repo-addressed list with an id-addressed link header on repositories never renamed. So what is recorded is that a hop happened on THIS request rather than what the last URL looks like.

Ended is the address the LAST request of the exchange went to, which on a followed hop is where that hop led. It is NOT a second witness, for the same reason: what it is for is resolving the successor once a hop has already been recorded, because the location that product sends addresses the repository by id and its canonical name is one read from there.

Refused is the address of a hop the redirect policy refused, answered beside that refusal with the status and headers that asked for it. A write's hop is refused, so this is where a family reads a successor without sending the write again.

func (*Response) Quote

func (r *Response) Quote(text string) string

Quote is text the instance answered in this exchange made safe to carry on an error a family builds from it, as Conn.Do makes a refusal's text safe: every secret the request carried redacted out of it, on one line, and bounded.

Jump to

Keyboard shortcuts

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