excon

package module
v0.0.0-...-8f7a788 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 9 Imported by: 0

README

go-ruby-excon/excon

excon — go-ruby-excon

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's excon gem — the fast, persistent HTTP client. It reproduces the reusable connection, the one-shot verb helpers, the option handling Excon performs around the wire (path / query building, header merging, Basic auth, the :expects status assertion, :idempotent retry), the Response (Status/Body/Headers/RemoteIp/ReasonPhrase), and the full Excon::Error tree — without any Ruby runtime.

It is the Excon client for go-embedded-ruby, but a standalone, reusable module — a sibling of go-ruby-faraday, go-ruby-regexp and go-ruby-erb.

What it is — and isn't. Everything Excon does around the socket is deterministic and needs no interpreter, so it lives here as pure Go: merging per-request options over the connection defaults, building the absolute URL (path plus an order-preserving, CGI-escaped query string), adding the Basic Authorization header, asserting the response status against :expects (raising the matching status error), and retrying an :idempotent request on a transport failure. The HTTP round-trip itself is a host seam: the Doer transport performs it. The default is NetHTTP, backed by net/http (it also captures the remote IP via httptrace); tests inject a DoerFunc stub or drive NetHTTP over an httptest server, and a host wires the real transport.

Features

Faithful port of the excon gem's client core, validated against Ruby on every platform where it is installed:

  • Persistent connectionexcon.New(url, Options{...}) bound to a base URL, reused across requests, with per-request overrides merged over the defaults.
  • One-shot verbsexcon.Get/Head/Delete/Post/Put/Patch(url, opts) and the connection methods conn.Get/Head/Delete/Post/Put/Patch(opts) plus the general conn.Request(opts).
  • OptionsHeaders, Query, Body, Path, Expects (status assertion), Idempotent/RetryLimit/RetryInterval, ReadTimeout/WriteTimeout/ ConnectTimeout, User/Password (Basic auth), and Middlewares.
  • ResponseStatus, Body, Headers, RemoteIp, ReasonPhrase, Success (mirrors Excon::Response).
  • :expects — a status not in the expected set raises the matching Excon::Error subclass by code (404 → NotFound, 422 → UnprocessableEntity, an unmapped 4xx → Client, 5xx → Server, …).
  • :idempotent retry — retries a socket / timeout transport failure up to RetryLimit attempts, sleeping RetryInterval between them.
  • Transport seamconn.Transport(Doer); NetHTTP() is the default net/http transport (capturing the peer IP), a DoerFunc a test stub. The core never opens a socket itself.
  • Error tree — the full Excon::Error hierarchy: Socket (Certificate), Timeout, ResponseParse, ProxyConnectionError, ProxyParse, TooManyRedirects, and the HTTPStatus subtree (Informational, Redirection, ClientBadRequest/Unauthorized/Forbidden/NotFound/…, ServerInternalServerError/BadGateway/GatewayTimeout/…), matched with errors.Is against the Err* sentinels (a superclass matches its subclasses).
  • UtilsEscape/Unescape (CGI-faithful, +-for-space), the ordered Query codec, BasicHeaderFrom, and a case-insensitive Headers.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian).

Install

go get github.com/go-ruby-excon/excon

Usage

package main

import (
	"errors"
	"fmt"

	"github.com/go-ruby-excon/excon"
)

func main() {
	conn := excon.New("https://api.example.com", excon.Options{
		Headers: excon.HeadersOf([2]string{"Accept", "application/json"}),
	})

	resp, err := conn.Get(excon.Options{
		Path:    "/widgets",
		Query:   excon.QueryOf([2]string{"q", "gadget"}),
		Expects: []int{200},
	})
	if err != nil {
		// a *excon.Error: use errors.Is(err, excon.ErrNotFound), IsServerError(err), …
		if errors.Is(err, excon.ErrNotFound) {
			fmt.Println("no such widget")
		}
		return
	}
	fmt.Println(resp.Status(), resp.Success(), resp.RemoteIp(), resp.Body())

	// one-shot form
	_, _ = excon.Post("https://api.example.com/widgets",
		excon.Options{Body: `{"name":"gadget"}`, Expects: []int{201}})
}
Injecting a transport (tests / hosts)
conn := excon.New("https://api.example.com").Transport(
	excon.DoerFunc(func(req *excon.Request) (*excon.Response, error) {
		return excon.NewResponse(200, `{"ok":true}`,
			excon.HeadersOf([2]string{"Content-Type", "application/json"}),
			"OK", "203.0.113.7"), nil
	}))

resp, _ := conn.Get(excon.Options{Path: "/ping", Expects: []int{200}})
// resp.Status() == 200, resp.RemoteIp() == "203.0.113.7"

Value model

gem this package
Excon.new(url, opts) New(url, Options{...})
Excon.get/post/...(url, opts) Get/Post/...(url, opts)
conn.request(method:, ...) conn.Request(Options{Method, ...})
conn.get/post/...(opts) conn.Get/Post/...(opts)
:headers / :query / :body Options.Headers / .Query / .Body
:expects / :idempotent Options.Expects / .Idempotent
:user / :password Options.User / .Password (Basic auth)
:middlewares Options.Middlewares ([]Middleware)
Excon::Response#status/body/remote_ip (*Response).Status()/Body()/RemoteIp()
Excon::Error subtree *Error + Err* sentinels (errors.Is)
Excon::Utils.query_string (*Query).Encode() / Escape / Unescape

Tests & coverage

The suite pairs deterministic tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential oracle against Ruby: the escape codec, ordered query building, and the Basic-auth header are diffed against the stdlib (CGI / Base64, exactly what Excon uses), and the error hierarchy is diffed against the real Excon::Error classes when the gem is installed. The oracle scripts skip themselves where ruby (or the gem) is absent. No test opens a remote socket — the transport is stubbed or driven over loopback httptest.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-excon/excon authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package excon is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's `excon` gem — the fast, persistent HTTP client. It models a reusable Connection bound to a base URL and default options, the one-shot verb helpers (Get, Post, …), the option handling Excon performs around the wire (path/query building, header merging, Basic auth, the :expects status assertion, and :idempotent retry), the Response (Status/Body/Headers/ RemoteIp/ReasonPhrase), and the full Excon::Error tree.

What it is — and isn't

Everything Excon does around the socket is deterministic and needs no interpreter, so it lives here as pure Go: merging per-request options over the connection defaults, building the absolute URL (path plus an order-preserving, CGI-escaped query string), adding the Basic Authorization header, asserting the response status against :expects (raising the matching status error), and retrying an :idempotent request on a transport failure. The HTTP round-trip itself is a host seam: the Doer transport performs it. The default is NetHTTP, backed by net/http (it also captures the remote IP via httptrace); tests inject a DoerFunc stub or drive NetHTTP over an httptest server, and a host (go-embedded-ruby / rbgo) wires the real transport.

Flow

conn := excon.New("https://api.example.com", excon.Options{
	Headers: excon.HeadersOf([2]string{"Accept", "application/json"}),
})

resp, err := conn.Get(excon.Options{
	Path:    "/widgets",
	Query:   excon.QueryOf([2]string{"q", "gadget"}),
	Expects: []int{200},
})
if err != nil { /* an excon.Error: NotFound, InternalServerError, Timeout, … */ }
_ = resp.Status()       // 200
_ = resp.Body()         // response body
_ = resp.RemoteIp()     // resolved peer IP
_ = resp.Success()      // true for 2xx

// one-shot form
resp, err = excon.Post("https://api.example.com/widgets",
	excon.Options{Body: `{"name":"gadget"}`, Expects: []int{201}})

Errors

An :expects mismatch raises a status Error whose Kind names the Excon subclass for the code (404 → KindNotFound, 500 → KindInternalServerError, an unmapped 4xx → KindClient, …); a transport failure raises KindSocket or KindTimeout. The whole tree matches with errors.Is via the sentinels: e.g. errors.Is(err, excon.ErrClient) matches any 4xx, errors.Is(err, excon.ErrHTTPStatus) any status error, errors.Is(err, excon.ErrError) any excon error — mirroring Ruby's rescue of a superclass. Helpers IsClientError, IsServerError, IsHTTPStatusError, IsSocketError and IsTimeout wrap the common checks.

Value model

Query params are an ordered Query (CGI-escaped like Excon::Utils.query_string); headers a case-insensitive ordered Headers; the body a string. A host maps its Ruby Excon::Connection / Response objects to and from these shapes.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrError                = &Error{Kind: KindError, Message: string(KindError)}
	ErrSocket               = &Error{Kind: KindSocket, Message: string(KindSocket)}
	ErrCertificate          = &Error{Kind: KindCertificate, Message: string(KindCertificate)}
	ErrTimeout              = &Error{Kind: KindTimeout, Message: string(KindTimeout)}
	ErrResponseParse        = &Error{Kind: KindResponseParse, Message: string(KindResponseParse)}
	ErrProxyConnectionError = &Error{Kind: KindProxyConnectionError, Message: string(KindProxyConnectionError)}
	ErrProxyParse           = &Error{Kind: KindProxyParse, Message: string(KindProxyParse)}
	ErrTooManyRedirects     = &Error{Kind: KindTooManyRedirects, Message: string(KindTooManyRedirects)}

	ErrHTTPStatus    = &Error{Kind: KindHTTPStatus, Message: string(KindHTTPStatus)}
	ErrInformational = &Error{Kind: KindInformational, Message: string(KindInformational)}
	ErrRedirection   = &Error{Kind: KindRedirection, Message: string(KindRedirection)}
	ErrClient        = &Error{Kind: KindClient, Message: string(KindClient)}
	ErrServer        = &Error{Kind: KindServer, Message: string(KindServer)}

	ErrBadRequest                   = &Error{Kind: KindBadRequest, Message: string(KindBadRequest)}
	ErrUnauthorized                 = &Error{Kind: KindUnauthorized, Message: string(KindUnauthorized)}
	ErrPaymentRequired              = &Error{Kind: KindPaymentRequired, Message: string(KindPaymentRequired)}
	ErrForbidden                    = &Error{Kind: KindForbidden, Message: string(KindForbidden)}
	ErrNotFound                     = &Error{Kind: KindNotFound, Message: string(KindNotFound)}
	ErrMethodNotAllowed             = &Error{Kind: KindMethodNotAllowed, Message: string(KindMethodNotAllowed)}
	ErrNotAcceptable                = &Error{Kind: KindNotAcceptable, Message: string(KindNotAcceptable)}
	ErrProxyAuthenticationRequired  = &Error{Kind: KindProxyAuthenticationRequired, Message: string(KindProxyAuthenticationRequired)}
	ErrRequestTimeout               = &Error{Kind: KindRequestTimeout, Message: string(KindRequestTimeout)}
	ErrConflict                     = &Error{Kind: KindConflict, Message: string(KindConflict)}
	ErrGone                         = &Error{Kind: KindGone, Message: string(KindGone)}
	ErrLengthRequired               = &Error{Kind: KindLengthRequired, Message: string(KindLengthRequired)}
	ErrPreconditionFailed           = &Error{Kind: KindPreconditionFailed, Message: string(KindPreconditionFailed)}
	ErrRequestEntityTooLarge        = &Error{Kind: KindRequestEntityTooLarge, Message: string(KindRequestEntityTooLarge)}
	ErrRequestURITooLong            = &Error{Kind: KindRequestURITooLong, Message: string(KindRequestURITooLong)}
	ErrUnsupportedMediaType         = &Error{Kind: KindUnsupportedMediaType, Message: string(KindUnsupportedMediaType)}
	ErrRequestedRangeNotSatisfiable = &Error{Kind: KindRequestedRangeNotSatisfiable, Message: string(KindRequestedRangeNotSatisfiable)}
	ErrExpectationFailed            = &Error{Kind: KindExpectationFailed, Message: string(KindExpectationFailed)}
	ErrUnprocessableEntity          = &Error{Kind: KindUnprocessableEntity, Message: string(KindUnprocessableEntity)}
	ErrTooManyRequests              = &Error{Kind: KindTooManyRequests, Message: string(KindTooManyRequests)}

	ErrInternalServerError = &Error{Kind: KindInternalServerError, Message: string(KindInternalServerError)}
	ErrNotImplemented      = &Error{Kind: KindNotImplemented, Message: string(KindNotImplemented)}
	ErrBadGateway          = &Error{Kind: KindBadGateway, Message: string(KindBadGateway)}
	ErrServiceUnavailable  = &Error{Kind: KindServiceUnavailable, Message: string(KindServiceUnavailable)}
	ErrGatewayTimeout      = &Error{Kind: KindGatewayTimeout, Message: string(KindGatewayTimeout)}
)

Sentinel errors for errors.Is matching. Each names an Excon error kind; a concrete Error with that Kind (or a descendant of it) matches via Error.Is.

Functions

func BasicHeaderFrom

func BasicHeaderFrom(user, password string) string

BasicHeaderFrom returns the Basic Authorization header value for a user and password: "Basic " followed by the newline-free base64 of "user:password", matching what Excon sets when :user/:password are supplied.

func Escape

func Escape(s string) string

Escape percent-encodes s the way Ruby's CGI.escape does (the escaper Excon applies to query keys and values): [A-Za-z0-9 _.-~] stays literal, a space becomes '+', and any other byte becomes %XX with upper-case hex.

func IsClientError

func IsClientError(err error) bool

IsClientError reports whether err is an Excon 4xx client status error.

func IsHTTPStatusError

func IsHTTPStatusError(err error) bool

IsHTTPStatusError reports whether err is an Excon HTTP status error (or any subclass), i.e. an error raised by an :expects mismatch.

func IsServerError

func IsServerError(err error) bool

IsServerError reports whether err is an Excon 5xx server status error.

func IsSocketError

func IsSocketError(err error) bool

IsSocketError reports whether err is an Excon socket (transport) error.

func IsTimeout

func IsTimeout(err error) bool

IsTimeout reports whether err is an Excon timeout error.

func Unescape

func Unescape(s string) string

Unescape reverses Escape the way Ruby's CGI.unescape does: '+' becomes a space and %XX becomes its byte. A truncated or invalid %XX is left literal.

Types

type Connection

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

Connection is a persistent, reusable HTTP client bound to a base URL and a set of default options, mirroring Excon::Connection. Build one with New; issue requests with Connection.Request or the verb helpers.

func New

func New(rawurl string, opts ...Options) *Connection

New builds a Connection for a base URL, mirroring Excon.new(url, opts). The URL's scheme, host, port, path, query and userinfo seed the connection defaults; the optional opts override them. The connection defaults to the net/http transport (NetHTTP); tests override it with Connection.Transport.

func (*Connection) Delete

func (c *Connection) Delete(opts ...Options) (*Response, error)

Delete issues a DELETE request (Excon::Connection#delete).

func (*Connection) Get

func (c *Connection) Get(opts ...Options) (*Response, error)

Get issues a GET request (Excon::Connection#get).

func (*Connection) Head

func (c *Connection) Head(opts ...Options) (*Response, error)

Head issues a HEAD request (Excon::Connection#head).

func (*Connection) Patch

func (c *Connection) Patch(opts ...Options) (*Response, error)

Patch issues a PATCH request (Excon::Connection#patch).

func (*Connection) Post

func (c *Connection) Post(opts ...Options) (*Response, error)

Post issues a POST request (Excon::Connection#post).

func (*Connection) Put

func (c *Connection) Put(opts ...Options) (*Response, error)

Put issues a PUT request (Excon::Connection#put).

func (*Connection) Request

func (c *Connection) Request(opts ...Options) (*Response, error)

Request issues a request, merging the given per-request options over the connection defaults, mirroring Excon::Connection#request. It builds the Request, runs it through the middleware stack around the retry/transport/ :expects core, and returns the Response (or an Error).

func (*Connection) Transport

func (c *Connection) Transport(d Doer) *Connection

Transport sets the connection's transport Doer (the host seam) and returns the connection for chaining. Tests inject a DoerFunc stub; rbgo wires the real transport.

type Doer

type Doer interface {
	Call(req *Request) (*Response, error)
}

Doer is the transport host seam: given a fully-resolved Request, it performs the HTTP round-trip and returns the Response, or a transport Error (Socket/Timeout). The core opens no socket itself — every request runs through whatever Doer the connection is set to, so tests inject a stub (see DoerFunc or drive NetHTTP over an httptest server) and rbgo wires the real transport.

type DoerFunc

type DoerFunc func(req *Request) (*Response, error)

DoerFunc adapts a function to the Doer interface, the convenient way to inject a stub transport in tests or a custom one in a host.

func (DoerFunc) Call

func (f DoerFunc) Call(req *Request) (*Response, error)

Call invokes f(req).

type Error

type Error struct {
	// Kind names the specific Excon error subclass (see the Err* sentinels).
	Kind ErrorKind
	// Message is the error text (Excon::Error#message).
	Message string
	// Request is the request context for a status error (nil otherwise).
	Request *Request
	// Response is the response context for a status error (nil otherwise).
	Response *Response
	// Cause is the underlying transport error for Socket/Timeout errors.
	Cause error
}

Error is the root of Excon's error tree (Excon::Error, aliased in the gem as Excon::Errors::Error). Every excon error carries a human message; a status error additionally carries the Request and Response that triggered it, and a transport error wraps the underlying cause.

The concrete kinds are distinguished by Error.Kind; the predicate helpers (IsHTTPStatusError, IsClientError, …) and the sentinel values (ErrNotFound, …) match with errors.Is, walking the class hierarchy exactly as Ruby's rescue of an Excon::Error subclass does.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface (Excon::Error#message).

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether e matches target: true when target is a *Error whose Kind is e's Kind or an ancestor of it. So errors.Is(err, ErrClient) matches any 4xx status error, errors.Is(err, ErrHTTPStatus) matches any status error, and errors.Is(err, ErrError) matches every excon error — mirroring Ruby's rescue of a superclass.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying transport cause for errors.Is/As.

type ErrorKind

type ErrorKind string

ErrorKind identifies an Excon error subclass, named as in the gem.

const (
	KindError ErrorKind = "Excon::Error"

	// Transport / non-status errors.
	KindSocket               ErrorKind = "Excon::Error::Socket"
	KindCertificate          ErrorKind = "Excon::Error::Certificate"
	KindTimeout              ErrorKind = "Excon::Error::Timeout"
	KindResponseParse        ErrorKind = "Excon::Error::ResponseParse"
	KindProxyConnectionError ErrorKind = "Excon::Error::ProxyConnectionError"
	KindProxyParse           ErrorKind = "Excon::Error::ProxyParse"
	KindTooManyRedirects     ErrorKind = "Excon::Error::TooManyRedirects"

	// HTTP status errors: base and range bases.
	KindHTTPStatus    ErrorKind = "Excon::Error::HTTPStatus"
	KindInformational ErrorKind = "Excon::Error::Informational"
	KindRedirection   ErrorKind = "Excon::Error::Redirection"
	KindClient        ErrorKind = "Excon::Error::Client"
	KindServer        ErrorKind = "Excon::Error::Server"

	// 4xx client status errors.
	KindBadRequest                   ErrorKind = "Excon::Error::BadRequest"
	KindUnauthorized                 ErrorKind = "Excon::Error::Unauthorized"
	KindPaymentRequired              ErrorKind = "Excon::Error::PaymentRequired"
	KindForbidden                    ErrorKind = "Excon::Error::Forbidden"
	KindNotFound                     ErrorKind = "Excon::Error::NotFound"
	KindMethodNotAllowed             ErrorKind = "Excon::Error::MethodNotAllowed"
	KindNotAcceptable                ErrorKind = "Excon::Error::NotAcceptable"
	KindProxyAuthenticationRequired  ErrorKind = "Excon::Error::ProxyAuthenticationRequired"
	KindRequestTimeout               ErrorKind = "Excon::Error::RequestTimeout"
	KindConflict                     ErrorKind = "Excon::Error::Conflict"
	KindGone                         ErrorKind = "Excon::Error::Gone"
	KindLengthRequired               ErrorKind = "Excon::Error::LengthRequired"
	KindPreconditionFailed           ErrorKind = "Excon::Error::PreconditionFailed"
	KindRequestEntityTooLarge        ErrorKind = "Excon::Error::RequestEntityTooLarge"
	KindRequestURITooLong            ErrorKind = "Excon::Error::RequestURITooLong"
	KindUnsupportedMediaType         ErrorKind = "Excon::Error::UnsupportedMediaType"
	KindRequestedRangeNotSatisfiable ErrorKind = "Excon::Error::RequestedRangeNotSatisfiable"
	KindExpectationFailed            ErrorKind = "Excon::Error::ExpectationFailed"
	KindUnprocessableEntity          ErrorKind = "Excon::Error::UnprocessableEntity"
	KindTooManyRequests              ErrorKind = "Excon::Error::TooManyRequests"

	// 5xx server status errors.
	KindInternalServerError ErrorKind = "Excon::Error::InternalServerError"
	KindNotImplemented      ErrorKind = "Excon::Error::NotImplemented"
	KindBadGateway          ErrorKind = "Excon::Error::BadGateway"
	KindServiceUnavailable  ErrorKind = "Excon::Error::ServiceUnavailable"
	KindGatewayTimeout      ErrorKind = "Excon::Error::GatewayTimeout"
)

The Excon error subclasses, named as in Excon::Error::*.

type Handler

type Handler func(req *Request) (*Response, error)

Handler is one step of the resolved request pipeline: it receives the Request, does its work (typically calling the next handler), and returns the Response or an error. The innermost handler is the connection core (retry + transport + :expects check); each Middleware wraps it.

type Headers

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

Headers is a case-insensitive, insertion-ordered header map, mirroring Excon::Headers. A lookup matches keys case-insensitively while the original casing of the first-seen key is preserved for iteration and display (so "Content-Type" and "content-type" address the same entry).

func HeadersOf

func HeadersOf(kv ...[2]string) *Headers

HeadersOf builds a Headers from ordered key/value pairs.

func NewHeaders

func NewHeaders() *Headers

NewHeaders returns an empty Headers.

func (*Headers) Clone

func (h *Headers) Clone() *Headers

Clone returns a shallow copy of h.

func (*Headers) Delete

func (h *Headers) Delete(key string)

Delete removes key (case-insensitive) if present, keeping the order of the remaining headers.

func (*Headers) Get

func (h *Headers) Get(key string) (string, bool)

Get returns the value for key (case-insensitive) and whether it was present.

func (*Headers) Has

func (h *Headers) Has(key string) bool

Has reports whether key is present (case-insensitive).

func (*Headers) Len

func (h *Headers) Len() int

Len reports the number of headers.

func (*Headers) Merge

func (h *Headers) Merge(other *Headers) *Headers

Merge overlays other's headers onto a copy of h and returns the result (an existing key is overwritten in place, a new key is appended), mirroring how a per-request :headers hash deep-merges over the connection's default headers.

func (*Headers) Pairs

func (h *Headers) Pairs() []Pair

Pairs returns the headers in insertion order (with first-seen key casing). The slice must not be mutated.

func (*Headers) Set

func (h *Headers) Set(key, val string)

Set inserts or replaces the value for key (case-insensitive). An existing key keeps its original casing and position; only the value is updated.

type Middleware

type Middleware func(next Handler) Handler

Middleware wraps a downstream Handler and returns a new one, mirroring an entry of Excon's :middlewares stack. The stack is composed outermost-first: a request-phase middleware acts before calling next, a response-phase middleware acts on the value next returns. Supply a stack via Options.Middlewares; the built-in retry (:idempotent) and status assertion (:expects) run inside it.

func RequestMiddleware

func RequestMiddleware(fn func(req *Request) error) Middleware

RequestMiddleware builds a request-phase Middleware: fn runs before the downstream handler and may rewrite the request or abort by returning an error.

func ResponseMiddleware

func ResponseMiddleware(fn func(resp *Response) error) Middleware

ResponseMiddleware builds a response-phase Middleware: fn runs on the Response the downstream handler returned (skipped when it errored) and may rewrite it or abort by returning an error.

type NetHTTPDoer

type NetHTTPDoer struct {
	// Client performs the request; defaults to http.DefaultClient.
	Client httpClient
}

NetHTTPDoer is the default Doer: it turns a Request into a net/http request, executes it with its Client, captures the remote IP via httptrace, and maps the response (or a transport failure) into a Response or a transport Error. A timeout maps to KindTimeout; any other transport failure to KindSocket.

func NetHTTP

func NetHTTP() *NetHTTPDoer

NetHTTP returns the default net/http-backed Doer.

func (*NetHTTPDoer) Call

func (d *NetHTTPDoer) Call(req *Request) (*Response, error)

Call performs the HTTP round-trip for req with net/http.

type Options

type Options struct {
	// Method is the HTTP method; defaults to "GET" when unset.
	Method string
	// Path overrides the request path (else the connection/URL path is used).
	Path string
	// Query is the URL query (:query); when non-nil it replaces the default.
	Query *Query
	// Headers are request headers (:headers); deep-merged over the defaults.
	Headers *Headers
	// Body is the request body (:body).
	Body string
	// Expects asserts the response status: a status not listed raises the
	// matching status [Error]. An empty/nil Expects performs no assertion.
	Expects []int
	// Idempotent enables automatic retry of transport failures (:idempotent).
	Idempotent bool
	// RetryLimit is the maximum number of attempts for an idempotent request
	// (:retry_limit); defaults to 4 on a connection.
	RetryLimit int
	// RetryInterval is the delay between idempotent retries, in milliseconds
	// (:retry_interval).
	RetryInterval int
	// ReadTimeout/WriteTimeout/ConnectTimeout are per-request timeouts, in
	// seconds, that a host transport may honour.
	ReadTimeout    int
	WriteTimeout   int
	ConnectTimeout int
	// User/Password set HTTP Basic credentials (:user/:password): a Basic
	// Authorization header is added unless one is already present.
	User     string
	Password string
	// Middlewares is the outer middleware stack (:middlewares) wrapped around the
	// built-in retry/expects core; when non-nil it replaces the default (none).
	Middlewares []Middleware
}

Options configures a request (and, on New, the connection defaults), mirroring the option hash Excon.new / Excon.get accept. A per-request Options is merged over the connection's defaults: :headers deep-merge, every other present field overrides. A zero field is treated as unset and inherits the connection default (so, e.g., an empty Body keeps the connection's).

type Pair

type Pair struct {
	Key string
	Val string
}

Pair is one entry of an insertion-ordered string-keyed map.

type Query

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

Query is the ordered set of URL query parameters passed as the :query option, mirroring the Hash (or String) Excon accepts. Keys are emitted in insertion order and may repeat (an array value in Excon becomes several pairs sharing a key). Encoding is byte-faithful to Excon::Utils.query_string.

func NewQuery

func NewQuery() *Query

NewQuery returns an empty Query.

func QueryOf

func QueryOf(kv ...[2]string) *Query

QueryOf builds a Query from ordered key/value pairs (each with a value).

func (*Query) Add

func (q *Query) Add(key, val string)

Add appends a key=value pair.

func (*Query) AddKey

func (q *Query) AddKey(key string)

AddKey appends a bare key with no value (Excon's nil-valued query entry).

func (*Query) Encode

func (q *Query) Encode() string

Encode renders the query as a leading-'?' string, byte-faithful to Excon::Utils.query_string: pairs in order, each key and value escaped with Escape (CGI.escape), a bare key emitted without '='. An empty query encodes to "".

func (*Query) Len

func (q *Query) Len() int

Len reports the number of pairs.

func (*Query) Pairs

func (q *Query) Pairs() []QueryPair

Pairs returns the pairs in insertion order. The slice must not be mutated.

type QueryPair

type QueryPair struct {
	Key    string
	Val    string
	HasVal bool
}

QueryPair is one entry of a Query: a key, its value, and whether a value is present. A key with no value (HasVal false) is emitted bare — matching Excon, which encodes a nil-valued query key as just the escaped key.

type Request

type Request struct {
	// Method is the upper-case HTTP method ("GET", "POST", …).
	Method string
	// URL is the absolute request URL, query string included.
	URL string
	// Headers are the outgoing request headers.
	Headers *Headers
	// Body is the outgoing request body ("" for none).
	Body string
	// ReadTimeout is the response-read timeout in seconds (0 = unset).
	ReadTimeout int
	// WriteTimeout is the request-write timeout in seconds (0 = unset).
	WriteTimeout int
	// ConnectTimeout is the connection-open timeout in seconds (0 = unset).
	ConnectTimeout int
}

Request is the fully-resolved HTTP request handed to the transport Doer and threaded through the middleware stack. A Connection builds it from the merged options: the method, the absolute URL (scheme/host/port + path + encoded query), the outgoing headers (including any Basic-auth header), the body, and the per-request timeouts a host transport may honour.

type Response

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

Response is the result of a request, mirroring Excon::Response: the status code, the body, the response headers, the reason phrase, and the remote IP the connection resolved to. It is produced by the transport Doer and returned unchanged (on a successful, expectation-satisfying request) to the caller.

func Delete

func Delete(rawurl string, opts ...Options) (*Response, error)

Delete issues a one-shot DELETE request (Excon.delete).

func Get

func Get(rawurl string, opts ...Options) (*Response, error)

Get issues a one-shot GET request (Excon.get).

func Head(rawurl string, opts ...Options) (*Response, error)

Head issues a one-shot HEAD request (Excon.head).

func NewResponse

func NewResponse(status int, body string, headers *Headers, reason, remoteIP string) *Response

NewResponse builds a Response from its parts. Transports (including a test stub) use it to report a completed round-trip.

func Patch

func Patch(rawurl string, opts ...Options) (*Response, error)

Patch issues a one-shot PATCH request (Excon.patch).

func Post

func Post(rawurl string, opts ...Options) (*Response, error)

Post issues a one-shot POST request (Excon.post).

func Put

func Put(rawurl string, opts ...Options) (*Response, error)

Put issues a one-shot PUT request (Excon.put).

func (*Response) Body

func (r *Response) Body() string

Body returns the response body (Excon::Response#body).

func (*Response) Headers

func (r *Response) Headers() *Headers

Headers returns the response headers (Excon::Response#headers).

func (*Response) ReasonPhrase

func (r *Response) ReasonPhrase() string

ReasonPhrase returns the response reason phrase (Excon::Response#reason_phrase).

func (*Response) RemoteIp

func (r *Response) RemoteIp() string

RemoteIp returns the resolved remote IP address (Excon::Response#remote_ip).

func (*Response) Status

func (r *Response) Status() int

Status returns the HTTP status code (Excon::Response#status).

func (*Response) Success

func (r *Response) Success() bool

Success reports whether the status is a 2xx (Excon treats 2xx as success).

Jump to

Keyboard shortcuts

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