Help us shape the future of Go! Take the 2026 Go Developer Survey

client

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT, Apache-2.0, MIT Imports: 17 Imported by: 0

Documentation

Overview

Package client provides an HTTP client with automatic 402 Payment Required handling.

Package client provides the generic HTTP 402 retry transport used by MPP payment methods.

NewHTTPClient returns a standard *http.Client that pays 402 Challenges and only follows same-origin redirects; Client and NewHTTPTransport take the same Options. Read the server's receipt from a paid response with mpp.ReceiptFromResponse.

Examples import this package by its bare name. The Tempo charge package uses the alias `charge`, so there is no conflict.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrPaymentDeclined = errors.New("mpp: payment declined")

ErrPaymentDeclined is returned, wrapped, when a payment is declined before a Credential is created: by an OnChallengeFunc that returns an error of its own, or by the MCP client's OnPaymentRequired hook. Check it with errors.Is.

Functions

func NewHTTPClient added in v0.6.0

func NewHTTPClient(methods []Method, opts ...Option) (*http.Client, error)

NewHTTPClient returns a standard *http.Client that pays 402 Challenges with methods, as Client.Do does: its Transport is the payment Transport and its CheckRedirect refuses every cross-origin redirect (after the hook and Timeout of an http.Client passed with WithHTTPClient, or net/http's 10-redirect limit), so a payment is only made to the origin the caller asked for. Configuration errors, such as unknown payment preference keys, are returned here rather than on the first request.

The http.Client passed with WithHTTPClient is copied, not modified; its Transport, or http.DefaultTransport, sends the requests. Unlike Client.Do, a request that times out after sending a credential reports net/http's timeout error rather than a PaymentOutcomeUnknownError.

Example
package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/tempoxyz/mpp-go/pkg/client"
	"github.com/tempoxyz/mpp-go/pkg/mpp"
)

// exampleMethod pays every "example/charge" challenge.
type exampleMethod struct{}

func (exampleMethod) Name() string   { return "example" }
func (exampleMethod) Intent() string { return mpp.IntentCharge }
func (exampleMethod) CreateCredential(_ context.Context, challenge *mpp.Challenge) (*mpp.Credential, error) {
	return &mpp.Credential{Challenge: challenge.ToEcho(), Payload: map[string]any{"paid": true}}, nil
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") == "" {
			challenge, err := mpp.NewChallengeWithError("secret", r.Host, "example", mpp.IntentCharge, map[string]any{"amount": "1"})
			if err != nil {
				panic(err)
			}
			header, err := mpp.FormatChallenge(challenge)
			if err != nil {
				panic(err)
			}
			w.Header().Set("WWW-Authenticate", header)
			w.WriteHeader(http.StatusPaymentRequired)
			return
		}
		fmt.Fprint(w, "paid content")
	}))
	defer server.Close()

	// A standard *http.Client: pass it to any library that takes one.
	httpClient, err := client.NewHTTPClient(
		[]client.Method{exampleMethod{}},
		client.WithMaxRetries(1),
	)
	if err != nil {
		panic(err)
	}
	resp, err := httpClient.Get(server.URL)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println(resp.StatusCode)
	// Read the server's receipt with mpp.ReceiptFromResponse(resp).
	

Types

type ChallengeCandidate added in v0.5.0

type ChallengeCandidate struct {
	Challenge *mpp.Challenge
	Method    Method
	// Quality is the payment preference q-value that ranked the Challenge.
	Quality float64
}

ChallengeCandidate pairs an offered Challenge with a configured Method that can pay it.

func SelectChallengeCandidates added in v0.5.0

func SelectChallengeCandidates(
	challenges []mpp.Challenge,
	methods []Method,
	preferences PaymentPreferences,
) ([]ChallengeCandidate, error)

SelectChallengeCandidates returns the offered Challenges that methods can pay, ranked like the HTTP Transport ranks them: by payment preference, then in offer order. Expired Challenges, Challenges no method handles and Challenges with q=0 are left out. Transports other than HTTP, such as MCP, use it to pick the Challenge to pay. Unknown preference keys and invalid q-values return an error.

type ChallengeMatcher added in v0.4.0

type ChallengeMatcher interface {
	CanHandleChallenge(challenge *mpp.Challenge) bool
}

ChallengeMatcher may be implemented by a Method that only handles some challenges for its method and intent. It allows multiple handlers for the same method and intent to select between method-specific request details.

type Client

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

Client is an HTTP client with automatic 402 payment handling.

func New

func New(methods []Method, opts ...Option) *Client

New creates a Client with the given payment methods.

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do sends an HTTP request, handling 402 responses automatically. If a 402 is received with a WWW-Authenticate: Payment header, it finds a matching method, creates credentials, and retries.

func (*Client) Get

func (c *Client) Get(ctx context.Context, url string) (*http.Response, error)

Get is a convenience method for GET requests.

func (*Client) Post

func (c *Client) Post(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error)

Post is a convenience method for POST requests.

type Method

type Method interface {
	// Name returns the method name this handler supports (e.g., "tempo").
	Name() string
	// Intent returns the payment intent this handler supports (e.g., "charge").
	Intent() string
	// CreateCredential creates a payment credential for the given challenge.
	CreateCredential(ctx context.Context, challenge *mpp.Challenge) (*mpp.Credential, error)
}

Method is the interface that payment methods must implement for client-side use.

type OnChallengeFunc added in v0.4.0

type OnChallengeFunc func(
	ctx context.Context,
	challenge *mpp.Challenge,
	createCredential func(ctx context.Context) (*mpp.Credential, error),
) (*mpp.Credential, error)

OnChallengeFunc is called when the transport has selected a 402 challenge it can pay, before any credential is created. It mirrors mppx's onChallenge.

createCredential creates a credential with the selected method; ctx can carry method-specific context for this request. It runs the method at most once per challenge: later calls return the first result. The hook can:

  • return (nil, nil) to let the transport create the credential itself, or to use the one createCredential already made,
  • return a credential, from createCredential or built another way, to send it,
  • return an error to decline the payment: RoundTrip returns that error, wrapped, and does not pay. It matches ErrPaymentDeclined only when no credential was created: after createCredential succeeded the payment may already have been made, so the error is not reported as a decline.

It runs once per challenge: a credential the transport already created for the same challenge is reused without calling it again.

type Option

type Option func(*Client)

Option configures a Client, and the http.Client and Transport that NewHTTPClient and NewHTTPTransport build.

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

WithHTTPClient sets a custom http.Client.

func WithMaxRetries added in v0.6.0

func WithMaxRetries(n int) Option

WithMaxRetries sets how many times one request pays a 402 Challenge and retries, like mppx's maxPaymentRetries. The default is 3; 0 never pays. A negative n is a configuration error.

func WithOnChallenge added in v0.4.0

func WithOnChallenge(onChallenge OnChallengeFunc) Option

WithOnChallenge sets a hook that runs when Do receives a 402 challenge it can pay, before a credential is created. Use it to approve or decline a payment or to supply the credential. See OnChallengeFunc.

func WithOrderChallenges added in v0.6.0

func WithOrderChallenges(fn OrderChallenges) Option

WithOrderChallenges sets a hook that filters and sorts the payable Challenges of each 402 response before one is paid. See OrderChallenges.

func WithPaymentPreferences added in v0.4.0

func WithPaymentPreferences(preferences PaymentPreferences) Option

WithPaymentPreferences sets client payment preferences. The preferences are advertised in Accept-Payment and used to rank server challenges. Unknown keys and invalid q-values cause Do to fail before sending the request.

type OrderChallenges added in v0.6.0

type OrderChallenges func(
	ctx context.Context,
	candidates []ChallengeCandidate,
) ([]ChallengeCandidate, error)

OrderChallenges filters and sorts the payable Challenges before one is paid, like mppx's orderChallenges. Candidates arrive ranked by payment preference, then offer order. Return them in the order to try: the first one returned that passes the origin checks is paid. Returning none pays nothing, and the 402 response is returned as is; returning an error fails the request with it.

type OriginValidator added in v0.6.0

type OriginValidator interface {
	ValidateOrigin(origin *url.URL, challenge *mpp.Challenge) error
}

OriginValidator may be implemented by a Method whose Credentials must only be created for a server that is entitled to them. The Tempo charge client implements it: a zero-amount Tempo proof authenticates the payer at the realm the Challenge names, so it is signed only for a realm the sending server owns. The HTTP Transport calls ValidateOrigin with the request URL and skips a Challenge it rejects; the MCP client calls it with a nil URL, since an MCP server has no origin.

A Method that does not implement it gets the Transport's built-in check for zero-amount tempo/charge Challenges.

type PaymentOutcomeUnknownError added in v0.5.1

type PaymentOutcomeUnknownError struct {
	// Challenge is the challenge the credential paid for. For a credential
	// the caller set on the request, it holds the challenge fields the
	// credential echoes (the request in RequestB64), or is nil when the
	// credential cannot be parsed.
	Challenge *mpp.Challenge
	// Credential is the credential sent with the failed request, or nil
	// when a caller-set credential cannot be parsed.
	Credential *mpp.Credential
	// Err is the error the paid request failed with.
	Err error
}

PaymentOutcomeUnknownError is returned by Transport.RoundTrip, and so by Client.Do wrapped in a *url.Error, when the request carrying a payment credential failed without a response. That includes a credential the caller set on the request itself (any header whose value uses the Payment scheme). The server may have settled the payment, so do not blindly retry: reconcile it using Challenge and Credential. It matches mppx's PaymentOutcomeUnknownError.

When an http.Client with a Timeout times out, net/http replaces the RoundTrip error with its own timeout error, so a bare Transport inside such a client cannot preserve this type. client.Client restores it: its Do returns a *url.Error whose Err is a PaymentOutcomeUnknownError wrapping the timeout, and Timeout still reports true.

func (*PaymentOutcomeUnknownError) Error added in v0.5.1

func (*PaymentOutcomeUnknownError) Timeout added in v0.5.1

func (e *PaymentOutcomeUnknownError) Timeout() bool

Timeout reports whether the paid request failed because it timed out.

func (*PaymentOutcomeUnknownError) Unwrap added in v0.5.1

func (e *PaymentOutcomeUnknownError) Unwrap() error

Unwrap returns the error the paid request failed with.

type PaymentPreferences added in v0.4.0

type PaymentPreferences map[string]float64

PaymentPreferences assigns HTTP q-values to configured payment methods. Keys use the "method/intent" form. Omitted methods default to q=1.

type Transport

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

Transport is an http.RoundTripper that handles 402 Payment Required responses. It wraps an inner transport and automatically negotiates payment.

func NewHTTPTransport added in v0.6.0

func NewHTTPTransport(methods []Method, inner http.RoundTripper, opts ...Option) (*Transport, error)

NewHTTPTransport returns a payment-aware http.RoundTripper that sends requests with inner (http.DefaultTransport when nil). It takes the same options as New, except WithHTTPClient, which it ignores, and returns configuration errors here rather than from RoundTrip.

A bare Transport refuses to pay after a redirect, since it cannot tell whether the caller chose that origin: use NewHTTPClient or Client for redirect-safe payments.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper with automatic 402 handling.

Jump to

Keyboard shortcuts

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