Documentation
¶
Overview ¶
Package querygo is a small, dependency-free SDK for the HTTP QUERY method (RFC 10008).
QUERY is a safe, idempotent request method that carries a request body describing a query to be evaluated against the target resource. Unlike GET, the query parameters travel in the body rather than the URI, and unlike POST, the request is safe and may be retried.
Sending a query ¶
client := querygo.NewClient(nil)
resp, err := client.QueryJSON(ctx, "https://example.org/search",
map[string]any{"q": "golang"}, querygo.MediaTypeJSON, nil)
if err != nil {
return err
}
defer resp.Body.Close()
The typed helpers QueryString, QueryBytes, QueryForm and QueryJSON build the request body for common content types. QueryJSONInto additionally decodes a JSON response:
var result SearchResult err := client.QueryJSONInto(ctx, url, request, &result, nil)
Response handling ¶
CheckStatus turns a non-2xx response into a *StatusError. ReadBody, DecodeText and DecodeJSON validate the status and consume the body.
Result location ¶
A QUERY response may advertise two distinct URIs (RFC 10008 Sections 2.3 and 2.4): Content-Location identifies the stored result of the query just run (FetchResult follows it), while Location identifies the equivalent resource whose GET re-runs the query for a current result (FetchEquivalent follows it).
resp, _ := client.QueryJSON(ctx, url, request, querygo.MediaTypeJSON, nil) result, err := client.FetchResult(ctx, resp, nil)
Discovery ¶
Options issues an OPTIONS request and reports QUERY support based on the Allow and Accept-Query header fields.
Retries ¶
Because QUERY is safe and idempotent, DoWithRetry can transparently retry transient failures, replaying the request body via Request.GetBody (which the Body constructors populate).
Example ¶
Example shows a full round trip: a Handler serving QUERY, and a Client sending a JSON query and decoding the JSON response.
server := httptest.NewServer(Handler{
AcceptQuery: []string{MediaTypeJSON},
Query: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(HeaderContentType, MediaTypeJSON)
_, _ = w.Write([]byte(`{"hits":2}`))
},
})
defer server.Close()
client := NewClient(server.Client())
var result struct {
Hits int `json:"hits"`
}
if err := client.QueryJSONInto(context.Background(), server.URL, map[string]string{"q": "golang"}, &result, nil); err != nil {
log.Fatal(err)
}
fmt.Println(result.Hits)
Output: 2
Index ¶
- Constants
- Variables
- func AdvertiseQuery(header http.Header, mediaTypes ...string)
- func CacheKey(targetURL, contentType string, body []byte) string
- func CacheKeyForBody(targetURL string, body Body) (string, error)
- func CheckStatus(resp *http.Response) error
- func DecodeJSON(resp *http.Response, v any) error
- func DecodeText(resp *http.Response) (string, error)
- func ExponentialBackoff(base, max time.Duration) func(retry int) time.Duration
- func NewRequest(ctx context.Context, req Request) (*http.Request, error)
- func ReadBody(resp *http.Response) ([]byte, error)
- func RetryOnTransient(resp *http.Response, err error) bool
- func SetResultLocation(header http.Header, uri string)
- func SupportsQuery(resp *http.Response) bool
- type Body
- type Client
- func (c *Client) Do(ctx context.Context, req Request) (*http.Response, error)
- func (c *Client) DoWithRetry(ctx context.Context, req Request, policy RetryPolicy) (*http.Response, error)
- func (c *Client) FetchEquivalent(ctx context.Context, resp *http.Response, header http.Header) (*http.Response, error)
- func (c *Client) FetchResult(ctx context.Context, resp *http.Response, header http.Header) (*http.Response, error)
- func (c *Client) Options(ctx context.Context, targetURL string, header http.Header) (Discovery, error)
- func (c *Client) Query(ctx context.Context, targetURL string, body io.Reader, contentType string) (*http.Response, error)
- func (c *Client) QueryBytes(ctx context.Context, targetURL, contentType string, value []byte, ...) (*http.Response, error)
- func (c *Client) QueryForm(ctx context.Context, targetURL string, values url.Values, accept string, ...) (*http.Response, error)
- func (c *Client) QueryJSON(ctx context.Context, targetURL string, value any, accept string, ...) (*http.Response, error)
- func (c *Client) QueryJSONInto(ctx context.Context, targetURL string, body any, result any, ...) error
- func (c *Client) QueryRequest(ctx context.Context, targetURL string, body Body, accept string, ...) (*http.Response, error)
- func (c *Client) QueryString(ctx context.Context, targetURL, contentType, value, accept string, ...) (*http.Response, error)
- type Discovery
- type Handler
- type Option
- type Request
- type ResponseInfo
- type RetryPolicy
- type StatusError
Examples ¶
Constants ¶
const ( HeaderAcceptQuery = "Accept-Query" HeaderAllow = "Allow" HeaderContentType = "Content-Type" HeaderContentLocation = "Content-Location" HeaderLocation = "Location" HeaderAccept = "Accept" )
Header field names used by the HTTP QUERY method.
const ( MediaTypeJSON = "application/json" MediaTypeForm = "application/x-www-form-urlencoded" )
Common media types used for QUERY request bodies.
const DefaultMaxRedirects = 10
DefaultMaxRedirects is the redirect limit used when Client.MaxRedirects is zero or negative.
const MaxErrorBodySnippet = 4 << 10 // 4 KiB
MaxErrorBodySnippet bounds how many bytes of a response body are captured in a StatusError message.
const MethodQuery = "QUERY"
MethodQuery is the HTTP QUERY method token defined by RFC 10008. QUERY is a safe, idempotent request method that carries a request body describing a query to be evaluated against the target resource.
Variables ¶
var ErrBodyNotReplayable = errors.New("querygo: body cannot be read without a GetReader")
ErrBodyNotReplayable is returned when a Body without a GetReader is asked to produce its bytes. The Body constructors (NewBodyString, NewBodyBytes, NewBodyForm, NewBodyJSON) always set GetReader.
var ErrNoEquivalentResource = errors.New("querygo: response has no equivalent resource location")
ErrNoEquivalentResource is returned when a QUERY response advertises no equivalent resource (no Location header field).
var ErrNoQueryResult = errors.New("querygo: response has no query result location")
ErrNoQueryResult is returned when a QUERY response advertises no result resource (no Content-Location header field).
var ErrRedirectBodyNotReplayable = errors.New("querygo: cannot follow redirect without a replayable body")
ErrRedirectBodyNotReplayable is returned when following a redirect requires resending the query body but the request has no GetBody to replay it. The Body constructors populate GetBody automatically.
var ErrTooManyRedirects = errors.New("querygo: too many redirects")
ErrTooManyRedirects is returned when a QUERY request exceeds the redirect limit (see Client.MaxRedirects).
Functions ¶
func AdvertiseQuery ¶
AdvertiseQuery sets the Accept-Query header field on a response, advertising the supported query media types as a Structured Fields List (RFC 10008 Section 3). It can be called from any handler (for example a GET handler) so clients discover QUERY support inline.
func CacheKey ¶
CacheKey computes a stable cache key for a QUERY request. Unlike GET, a QUERY cache entry must be keyed on the request content in addition to the method and URI, because the query travels in the body. The returned value is the hex-encoded SHA-256 of those inputs.
Example ¶
ExampleCacheKey shows that the cache key depends on the request body, so two different queries to the same URL produce different keys.
a := CacheKey("https://example.org/search", MediaTypeJSON, []byte(`{"q":"go"}`))
b := CacheKey("https://example.org/search", MediaTypeJSON, []byte(`{"q":"rust"}`))
fmt.Println(a == b)
Output: false
func CacheKeyForBody ¶
CacheKeyForBody is CacheKey using a Body, materializing its content.
func CheckStatus ¶
CheckStatus returns a *StatusError when resp carries a non-2xx status code, otherwise nil. On error it reads a bounded snippet of the body for the error message but does not close the body; the caller remains responsible for closing resp.Body.
func DecodeJSON ¶
DecodeJSON validates the response status and decodes the JSON body into v. The body is always closed. A nil v drains and validates only.
func DecodeText ¶
DecodeText validates the response status and returns the body as a string. The body is always closed.
func ExponentialBackoff ¶
ExponentialBackoff returns a backoff function that doubles the base delay on each retry, capped at max.
func NewRequest ¶
NewRequest builds an *http.Request that uses the QUERY method for the given Request. When GetBody is set it is wired onto the request so the standard library can replay the body across redirects.
func ReadBody ¶
ReadBody validates the response status and returns the full response body. The body is always closed.
func RetryOnTransient ¶
RetryOnTransient retries transport errors (except context cancellation) and the 429, 502, 503 and 504 status codes.
func SetResultLocation ¶
SetResultLocation sets the Content-Location header field on a response, identifying the GET-retrievable resource that holds the query result. This is the server-side counterpart to Client.FetchResult.
func SupportsQuery ¶
SupportsQuery reports whether a response advertises support for the QUERY method, either by listing QUERY in Allow or by carrying an Accept-Query header field.
Types ¶
type Body ¶
Body pairs a request content reader with its media type. When GetReader is set the body can be produced again, enabling redirect and retry replay.
func NewBodyBytes ¶
NewBodyBytes builds a Body from a byte slice with the given content type.
func NewBodyForm ¶
NewBodyForm builds an application/x-www-form-urlencoded Body from form values.
func NewBodyJSON ¶
NewBodyJSON marshals value to JSON and builds an application/json Body.
func NewBodyString ¶
NewBodyString builds a Body from a string with the given content type.
type Client ¶
type Client struct {
// HTTPClient performs the underlying HTTP round trips. When nil,
// http.DefaultClient is used.
HTTPClient *http.Client
// BaseURL, when set, is used to resolve relative request URLs. Absolute
// request URLs are used as-is.
BaseURL string
// DefaultHeader holds header fields applied to every request unless the
// request already carries a value for that field.
DefaultHeader http.Header
// UserAgent, when set, is applied as the User-Agent header field unless a
// request already provides one.
UserAgent string
// MaxRedirects caps the number of redirects followed for a QUERY request.
// When zero or negative, DefaultMaxRedirects is used. Redirects are handled
// per RFC 10008 Section 2.5: 301, 302, 307 and 308 re-issue QUERY (replaying
// the body), while 303 switches to GET.
MaxRedirects int
}
Client sends HTTP QUERY requests. The zero value is usable and falls back to http.DefaultClient, but NewClient is the preferred constructor.
func NewClient ¶
NewClient returns a Client using the provided HTTP client (or http.DefaultClient when nil) configured with the given options.
func (*Client) Do ¶
Do sends a QUERY request and returns the raw HTTP response. The caller is responsible for closing the response body.
func (*Client) DoWithRetry ¶
func (c *Client) DoWithRetry(ctx context.Context, req Request, policy RetryPolicy) (*http.Response, error)
DoWithRetry sends a QUERY request, retrying according to policy. Between retries the previous response body is drained and closed and the request body is replayed via Request.GetBody. If the request carries a non-replayable body (Body set without GetBody), no retry is attempted.
The caller is responsible for closing the returned response body.
func (*Client) FetchEquivalent ¶
func (c *Client) FetchEquivalent(ctx context.Context, resp *http.Response, header http.Header) (*http.Response, error)
FetchEquivalent performs a GET on the equivalent resource for the query, as advertised by the Location response field (RFC 10008 Section 2.4). Unlike FetchResult, this re-runs the query without resending its content and yields the current result. The URI is resolved relative to the original request URL. It returns ErrNoEquivalentResource when the response carries no Location.
The caller is responsible for closing the returned response body.
func (*Client) FetchResult ¶
func (c *Client) FetchResult(ctx context.Context, resp *http.Response, header http.Header) (*http.Response, error)
FetchResult performs a GET on the resource holding the result of the query that was just performed, as advertised by the Content-Location response field (RFC 10008 Section 2.3). The URI is resolved relative to the original request URL. It returns ErrNoQueryResult when the response carries no Content-Location.
The caller is responsible for closing the returned response body.
func (*Client) Options ¶
func (c *Client) Options(ctx context.Context, targetURL string, header http.Header) (Discovery, error)
Options issues an HTTP OPTIONS request to targetURL and interprets the response header fields to determine QUERY support. The response body is drained and closed.
Example ¶
ExampleClient_Options shows discovering QUERY support via an OPTIONS request.
server := httptest.NewServer(Handler{AcceptQuery: []string{MediaTypeJSON}})
defer server.Close()
client := NewClient(server.Client())
discovery, err := client.Options(context.Background(), server.URL, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(discovery.SupportsQuery, discovery.AcceptQuery[0])
Output: true application/json
func (*Client) Query ¶
func (c *Client) Query(ctx context.Context, targetURL string, body io.Reader, contentType string) (*http.Response, error)
Query is a convenience wrapper that sends a QUERY request with a body and Content-Type. Use Do or the typed Query* helpers for more control.
func (*Client) QueryBytes ¶
func (c *Client) QueryBytes(ctx context.Context, targetURL, contentType string, value []byte, accept string, header http.Header) (*http.Response, error)
QueryBytes sends a QUERY request with a byte-slice body.
func (*Client) QueryForm ¶
func (c *Client) QueryForm(ctx context.Context, targetURL string, values url.Values, accept string, header http.Header) (*http.Response, error)
QueryForm sends a QUERY request with a form-encoded body.
func (*Client) QueryJSON ¶
func (c *Client) QueryJSON(ctx context.Context, targetURL string, value any, accept string, header http.Header) (*http.Response, error)
QueryJSON sends a QUERY request whose body is value marshaled to JSON.
func (*Client) QueryJSONInto ¶
func (c *Client) QueryJSONInto(ctx context.Context, targetURL string, body any, result any, header http.Header) error
QueryJSONInto sends a QUERY request with a JSON body and decodes the JSON response into result. It is a convenience over QueryJSON + DecodeJSON.
type Discovery ¶
type Discovery struct {
// SupportsQuery is true when the response advertises the QUERY method via
// the Allow header field or carries an Accept-Query header field.
SupportsQuery bool
// Allow lists the methods advertised in the Allow header field.
Allow []string
// AcceptQuery lists the query media types advertised in the Accept-Query
// header field.
AcceptQuery []string
// Header is the raw response header.
Header http.Header
}
Discovery describes a server's advertised support for the QUERY method, as reported by an OPTIONS response (or any response carrying the relevant header fields).
func DiscoveryFromResponse ¶
DiscoveryFromResponse interprets a response's header fields as QUERY support metadata.
func (Discovery) AcceptsMediaType ¶
AcceptsMediaType reports whether mediaType is covered by the advertised Accept-Query media ranges, honoring the "*/*" and "type/*" wildcards.
type Handler ¶
type Handler struct {
// Query handles QUERY requests. Required. The request body carries the
// query to evaluate.
Query http.HandlerFunc
// AcceptQuery lists the query media types advertised via the Accept-Query
// header field (on OPTIONS and 405 responses). When non-empty, QUERY
// requests whose Content-Type is not in the list are rejected with 415
// Unsupported Media Type.
AcceptQuery []string
// AllowedMethods lists additional methods (besides QUERY and OPTIONS) that
// the resource supports, advertised via the Allow header field.
AllowedMethods []string
// OnOptions, when set, handles OPTIONS requests instead of the built-in
// response.
OnOptions http.HandlerFunc
}
Handler serves the HTTP QUERY method for a resource. It dispatches QUERY requests to Query, answers OPTIONS with the advertised QUERY support, and rejects other methods with 405 Method Not Allowed. The zero value is not useful; set Query (see NewHandler).
Handler implements http.Handler and can be mounted on any router, including the standard http.ServeMux (works on Go 1.21+, which lacks method-based route patterns).
func NewHandler ¶
func NewHandler(query http.HandlerFunc, acceptQuery ...string) Handler
NewHandler returns a Handler for the given QUERY handler function, advertising the provided query media types via Accept-Query.
type Option ¶
type Option func(*Client)
Option customizes a Client during construction.
func WithBaseURL ¶
WithBaseURL sets the base URL used to resolve relative request URLs.
func WithDefaultHeader ¶
WithDefaultHeader sets header fields applied to every request unless already present. The provided header is cloned.
func WithHeader ¶
WithHeader adds a single default header field, allocating the header map if needed.
func WithMaxRedirects ¶ added in v0.2.0
WithMaxRedirects sets the maximum number of redirects followed for a QUERY request. A value of zero or less selects DefaultMaxRedirects.
func WithUserAgent ¶
WithUserAgent sets the default User-Agent header field.
type Request ¶
type Request struct {
// URL is the target URL. It may be relative when the client has a BaseURL.
URL string
// Body is the request content (the query). It may be nil for an empty
// query.
Body io.Reader
// GetBody, when set, returns a fresh Body reader. It enables the request
// to be replayed for redirects and retries. Body constructors such as
// NewBodyString populate it automatically via RequestFromBody.
GetBody func() (io.Reader, error)
// ContentType sets the Content-Type header field describing Body.
ContentType string
// Accept sets the Accept header field advertising acceptable response
// media types.
Accept string
// Header holds additional header fields to add to the request.
Header http.Header
}
Request describes a single HTTP QUERY request.
type ResponseInfo ¶
type ResponseInfo struct {
StatusCode int
ContentLocation string
Location string
AcceptQuery []string
Allow []string
Header http.Header
}
ResponseInfo captures the QUERY-relevant metadata of a response.
func ParseResponseInfo ¶
func ParseResponseInfo(resp *http.Response) ResponseInfo
ParseResponseInfo extracts QUERY-relevant metadata from a response.
func (ResponseInfo) EquivalentResourceURI ¶
func (info ResponseInfo) EquivalentResourceURI() string
EquivalentResourceURI returns the URI of the equivalent resource for the query, as advertised by the Location response field (RFC 10008 Section 2.4). A GET to this URI re-runs the same query without resending the content, yielding the current result. It is empty when the response carries no such field.
func (ResponseInfo) ResultURI ¶
func (info ResponseInfo) ResultURI() string
ResultURI returns the URI of the resource holding the result of the query that was just performed, as advertised by the Content-Location response field (RFC 10008 Section 2.3). A GET to this URI retrieves that stored result. It is empty when the response carries no such field.
type RetryPolicy ¶
type RetryPolicy struct {
// MaxAttempts is the total number of attempts, including the first. Values
// below 1 are treated as 1.
MaxAttempts int
// Backoff returns the delay to wait before a retry. Its argument is the
// 1-indexed retry number (1 before the first retry). When nil, retries
// happen without delay.
Backoff func(retry int) time.Duration
// RetryOn decides whether the outcome of an attempt should be retried.
// When nil, no attempt is retried.
RetryOn func(resp *http.Response, err error) bool
}
RetryPolicy configures automatic retries for QUERY requests. Because QUERY is a safe and idempotent method, transient failures may be retried without changing server state, provided the request body can be replayed (Request carries a GetBody, which the Body constructors populate).
func DefaultRetryPolicy ¶
func DefaultRetryPolicy() RetryPolicy
DefaultRetryPolicy returns a policy that makes up to 3 attempts with exponential backoff, retrying transient transport errors and the 429/502/ 503/504 status codes.
type StatusError ¶
type StatusError struct {
StatusCode int
Status string
Method string
URL string
Body []byte
Header http.Header
}
StatusError is returned when a QUERY (or result) response carries a status code outside the 2xx success range.
func AsStatusError ¶
func AsStatusError(err error) (*StatusError, bool)
AsStatusError reports whether err wraps a *StatusError and returns it.
func (*StatusError) Error ¶
func (e *StatusError) Error() string
Error implements the error interface.