typhoeus

package module
v0.0.0-...-19c93ad 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: 8 Imported by: 0

README

go-ruby-typhoeus/typhoeus

typhoeus — go-ruby-typhoeus

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the client model of Ruby's typhoeus gem — the parallel HTTP client. It reproduces the request/response objects, the one-shot verb helpers, the on-complete callbacks, the libcurl-style return codes, and — the gem's signature feature — the Hydra parallel runner, all backed by Go's net/http and goroutines instead of libcurl. No Ruby runtime, no libcurl, no cgo.

It is the Typhoeus 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 Typhoeus models around the wire is deterministic and needs no interpreter and no libcurl, so it lives here as pure Go: the Request with its Options (params, body, headers, userpwd, timeout, followlocation), the on-complete callbacks, the Response (code, body, headers, total_time, Success/TimedOut, return_code), and the parallel Hydra runner. The HTTP round-trip itself is a host seam: a Transport performs the transfer. The default production Transport is NetHTTP, backed by net/http; tests inject a TransportFunc stub and the model — the Hydra's parallelism included — never opens a socket. This mirrors the gem, whose Ethon adapter is the only piece that touches the network.

Features

Faithful port of the typhoeus gem's client model, its escaping validated against the gem (Ethon/libcurl) on every platform where it is installed:

  • One-shot verbsGet/Post/Put/Delete/Head/Patch(url, Options), each returning a *Response (mirroring Typhoeus.get(url, options)).
  • RequestNewRequest(url, method, Options) with method, url and Options{Params, Body, Headers, UserPwd, Timeout, FollowLocation, MaxRedirects}, OnComplete callbacks, and Run (mirroring Typhoeus::Request).
  • Hydra — the parallel runner: NewHydra(HydraOptions{MaxConcurrency}), Queue(req), then Run() executes the queue concurrently (goroutines, bounded by MaxConcurrency) and fires each request's callbacks in queue order. A callback may itself Queue more requests, which run in a following round; Abort() stops further rounds. Deterministic and leak-free.
  • ResponseCode, Body, Headers, TotalTime, ReturnCode, plus Success() (2xx + :ok), TimedOut() and ReturnMessage().
  • ReturnCode — the libcurl result Typhoeus surfaces as return_code (:ok, :couldnt_resolve_host, :couldnt_connect, :operation_timedout, :recv_error, :internal_error), classified from the transport failure.
  • Transport seamreq.Transport / DefaultTransport; NetHTTP() is the default net/http transport (honouring timeout, followlocation, maxredirs, basic userpwd), a TransportFunc a test stub. The model never opens a socket.
  • Ordered Params / case-insensitive Headers and curl-style Escape / Unescape / BuildQuery / ParseQuery (space → %20, RFC-3986 unreserved).

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-typhoeus/typhoeus

Usage

One-shot
package main

import (
	"fmt"

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

func main() {
	resp := typhoeus.Get("https://api.example.com/widgets",
		typhoeus.Options{Params: typhoeus.ParamsOf([2]string{"q", "go ruby"})})

	fmt.Println(resp.Code)      // 200
	fmt.Println(resp.Success()) // true for a 2xx with return_code == :ok
	fmt.Println(resp.Body)      // response body string
}
Parallel (Hydra)
hydra := typhoeus.NewHydra(typhoeus.HydraOptions{MaxConcurrency: 5})
for _, u := range urls {
	req := typhoeus.NewRequest(u, "GET")
	req.OnComplete(func(r *typhoeus.Response) {
		fmt.Println(r.Request().URL, r.Code, r.TotalTime)
	})
	hydra.Queue(req)
}
hydra.Run() // runs them concurrently, then fires callbacks in queue order
Injecting a transport (tests / hosts)
req := typhoeus.NewRequest("https://api.example.com/ping", "GET")
req.Transport = typhoeus.TransportFunc(func(r *typhoeus.Request) *typhoeus.Response {
	return &typhoeus.Response{Code: 200, Body: "pong", ReturnCode: typhoeus.ReturnOK}
})
resp := req.Run() // resp.Body == "pong", no socket opened

Value model

gem this package
Typhoeus.get(url, options) Get(url, Options{...}) (Post/Put/…)
Typhoeus::Request.new(url, method:, ...) NewRequest(url, method, Options{...})
request.on_complete { |resp| ... } req.OnComplete(func(*Response) { ... })
request.run req.Run()
Typhoeus::Hydra.new(max_concurrency: n) NewHydra(HydraOptions{MaxConcurrency: n})
hydra.queue(req) / hydra.run hydra.Queue(req) / hydra.Run()
resp.code / body / headers / total_time resp.Code / Body / Headers / TotalTime
resp.success? / timed_out? / return_code resp.Success() / TimedOut() / ReturnCode
Ethon easy.escape (libcurl) Escape / Unescape / BuildQuery

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential oracle against the reference typhoeus gem: the curl-style percent-escaping is diffed byte-for-byte against the gem's Ethon::Easy#escape (libcurl). The oracle skips itself where the gem is absent. The Hydra's parallelism is covered deterministically — a semaphore barrier proves the MaxConcurrency bound and that Run leaks no goroutine — and the whole suite is race-clean (go test -race). Only localhost httptest servers back the net/http integration path; the transport is a stub everywhere else.

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-typhoeus/typhoeus 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 typhoeus is a pure-Go (CGO-free) reimplementation of the client model of Ruby's `typhoeus` gem — the parallel HTTP client that, in Ruby, drives libcurl (via Ethon) to run many requests concurrently. Here the same object model (Request, Response, Hydra, the one-shot verb helpers, on-complete callbacks and the libcurl-style return codes) is backed by Go's net/http and goroutines instead of libcurl, with no cgo and no libcurl dependency.

What it is — and isn't

Everything Typhoeus models around the wire is deterministic and needs no interpreter and no libcurl, so it lives here as pure Go: the Request with its Options (params, body, headers, userpwd, timeout, followlocation), the on-complete callbacks, the Response (code, body, headers, total_time, success?/timed_out?, return_code), and — the gem's signature feature — the parallel Hydra runner that executes a queue of requests concurrently with a bounded max-concurrency and fires their callbacks.

The HTTP round-trip itself is a host seam: a Transport performs the transfer. The default production Transport is NetHTTP, backed by net/http; tests inject a TransportFunc stub, so the deterministic suite drives the whole model — including the Hydra's parallelism — without opening a socket, and the concrete net/http mapping (libcurl return-code classification, redirect and timeout policy) is exercised through the same seam. This mirrors the gem, whose Ethon adapter is the only piece that touches the network.

One-shot

resp := typhoeus.Get("https://api.example.com/widgets",
	typhoeus.Options{Params: typhoeus.ParamsOf([2]string{"q", "go ruby"})})
_ = resp.Code       // 200
_ = resp.Body       // response body string
_ = resp.Success()  // true for a 2xx with return_code == :ok

Parallel (Hydra)

hydra := typhoeus.NewHydra(typhoeus.HydraOptions{MaxConcurrency: 5})
for _, u := range urls {
	req := typhoeus.NewRequest(u, "GET")
	req.OnComplete(func(r *typhoeus.Response) { collect(r) })
	hydra.Queue(req)
}
hydra.Run() // runs them concurrently, then fires callbacks in queue order

Value model

Query params and url-encoded form bodies are carried as an ordered string→string Params; headers as a case-insensitive ordered Headers. A host (go-embedded-ruby / rbgo) maps its Ruby Typhoeus::Request / Response / Hydra objects to and from these shapes.

Index

Constants

View Source
const DefaultMaxConcurrency = 200

DefaultMaxConcurrency is the number of requests a Hydra runs at once when no MaxConcurrency is given, matching Typhoeus::Hydra's default of 200.

Variables

This section is empty.

Functions

func BuildQuery

func BuildQuery(params *Params) string

BuildQuery renders params as an application/x-www-form-urlencoded query string: the params are emitted in insertion order (Typhoeus preserves the params' order rather than sorting) and each key and value is run through Escape.

func Escape

func Escape(s string) string

Escape percent-encodes s the way libcurl's curl_easy_escape (Ethon::Easy#escape) does: the unreserved set [A-Za-z0-9-._~] is left literal and every other byte, a space included, becomes %XX with upper-case hex.

func Unescape

func Unescape(s string) string

Unescape reverses Escape: %XX becomes its byte. An invalid or truncated %XX is left literal, matching libcurl's tolerant decoder (curl_easy_unescape). A '+' is left as-is (unlike form decoding), since curl's escaping emits %20 for a space.

Types

type Headers

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

Headers is a case-insensitive, insertion-ordered header map used for a request's outgoing headers and a response's 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), mirroring the header hash Typhoeus surfaces on a response.

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) 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 Hydra

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

Hydra is the parallel runner — Typhoeus's signature feature. Requests are queued with Hydra.Queue and executed by Hydra.Run, which runs them concurrently (bounded by MaxConcurrency) and then fires each request's on-complete callbacks in queue order, mirroring Typhoeus::Hydra.

A Hydra is not safe for concurrent use by multiple goroutines; queue from one goroutine and call Run. Run itself is where the parallelism happens.

func NewHydra

func NewHydra(opts ...HydraOptions) *Hydra

NewHydra builds a Hydra, mirroring Typhoeus::Hydra.new. With no options (or MaxConcurrency <= 0) it uses DefaultMaxConcurrency.

func (*Hydra) Abort

func (h *Hydra) Abort()

Abort stops the hydra from starting further rounds, mirroring Typhoeus::Hydra#abort. Requests already in flight in the current round finish.

func (*Hydra) Queue

func (h *Hydra) Queue(r *Request)

Queue adds a request to the hydra, mirroring Typhoeus::Hydra#queue. A request may also be queued from within an on-complete callback during Hydra.Run; it is run in a following round.

func (*Hydra) QueuedCount

func (h *Hydra) QueuedCount() int

QueuedCount reports how many requests are currently queued (Typhoeus::Hydra#queued_requests size).

func (*Hydra) Run

func (h *Hydra) Run()

Run executes the queued requests concurrently and then fires their on-complete callbacks in queue order, mirroring Typhoeus::Hydra#run. It blocks until every request in flight has completed — no goroutine outlives the call — and returns once the queue is drained (or Hydra.Abort was called).

Concurrency is bounded to MaxConcurrency by a semaphore; each request writes only its own slot of the results slice, and Run waits for the whole round before reading any result, so the transports run in parallel without a data race and without a leaked goroutine. Callbacks then run on the calling goroutine, in queue order, so results are deterministic; a callback may queue more requests, which are executed in the next round.

type HydraOptions

type HydraOptions struct {
	// MaxConcurrency bounds how many queued requests run at once; <= 0 selects
	// [DefaultMaxConcurrency].
	MaxConcurrency int
}

HydraOptions configures a Hydra, mirroring Typhoeus::Hydra.new(max_concurrency:).

type NetHTTPTransport

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

NetHTTPTransport is the default Transport: it turns a Request into a net/http request, executes it under an http.Client configured from the request's Options (timeout and redirect policy), and maps the response — or a transport failure — onto a Response, classifying a failure into a libcurl ReturnCode.

func NetHTTP

func NetHTTP() *NetHTTPTransport

NetHTTP returns the default net/http-backed Transport.

func (*NetHTTPTransport) Run

func (t *NetHTTPTransport) Run(req *Request) *Response

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

type Options

type Options struct {
	// Params are query parameters appended to the URL (curl-escaped, ordered).
	Params *Params
	// Body is the request body: a string sent verbatim, a [*Params] form-encoded
	// as application/x-www-form-urlencoded, nil for no body, or any other value
	// rendered with fmt.Sprint.
	Body any
	// Headers are the outgoing request headers.
	Headers *Headers
	// UserPwd is the HTTP Basic credential as "login:password" (curl's :userpwd);
	// when set it becomes an Authorization: Basic header.
	UserPwd string
	// Timeout is the overall transfer timeout; 0 means no timeout.
	Timeout time.Duration
	// FollowLocation, when true, follows 3xx redirects (curl's :followlocation).
	// When false the redirect response is returned as-is.
	FollowLocation bool
	// MaxRedirects caps the number of redirects to follow when FollowLocation is
	// true (curl's :maxredirs); 0 leaves the transport default.
	MaxRedirects int
}

Options carries the per-request settings, mirroring the options Hash passed to Typhoeus::Request.new(url, method:, params:, body:, headers:, userpwd:, timeout:, followlocation:). Every field is optional; the zero value requests a plain transfer with the transport's defaults.

type Pair

type Pair struct {
	Key string
	Val string
}

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

type Params

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

Params is an insertion-ordered string→string map used for request query parameters and url-encoded form bodies. Ruby's Typhoeus threads plain Hashes through the flow; this ordered map mirrors that, giving deterministic, order-preserving output for query strings (see BuildQuery).

func NewParams

func NewParams() *Params

NewParams returns an empty ordered Params.

func ParamsOf

func ParamsOf(kv ...[2]string) *Params

ParamsOf builds a Params from ordered key/value pairs (later duplicates overwrite earlier values, keeping the first position — Ruby Hash semantics).

func ParseQuery

func ParseQuery(query string) *Params

ParseQuery decodes an application/x-www-form-urlencoded query string into an ordered Params: each key and value is [Unescape]d, a bare key (no '=') maps to the empty string, an empty segment is skipped, and a later duplicate key overwrites an earlier one (keeping its position). A leading '?' is ignored.

func (*Params) Clone

func (p *Params) Clone() *Params

Clone returns a shallow copy of p.

func (*Params) Delete

func (p *Params) Delete(key string)

Delete removes key if present, keeping the order of the remaining entries.

func (*Params) Encode

func (p *Params) Encode() string

Encode renders the params as an order-preserving, curl-escaped query string (see BuildQuery).

func (*Params) Get

func (p *Params) Get(key string) (string, bool)

Get returns the value for key and whether it was present.

func (*Params) Has

func (p *Params) Has(key string) bool

Has reports whether key is present.

func (*Params) Len

func (p *Params) Len() int

Len reports the number of entries.

func (*Params) Pairs

func (p *Params) Pairs() []Pair

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

func (*Params) Set

func (p *Params) Set(key, val string)

Set inserts or replaces the entry for key, preserving the position of an existing key when it is overwritten (last write wins on value, first write wins on order — the gem's Hash semantics).

type Request

type Request struct {
	// Method is the upper-case HTTP method ("GET", "POST", …).
	Method string
	// URL is the request URL (query params from Options are appended to it).
	URL string
	// Options are the per-request settings.
	Options Options
	// Transport, when non-nil, overrides [DefaultTransport] for this request — the
	// injectable seam tests use to avoid the network.
	Transport Transport
	// contains filtered or unexported fields
}

Request is a single HTTP request, mirroring Typhoeus::Request: a method, a URL, the per-request Options, and any number of on-complete callbacks. Run it directly with Request.Run, or queue it on a Hydra to run in parallel with others.

func NewRequest

func NewRequest(url, method string, opts ...Options) *Request

NewRequest builds a Request for url with the given method and optional Options, mirroring Typhoeus::Request.new. The method is upper-cased.

func (*Request) OnComplete

func (r *Request) OnComplete(cb func(*Response))

OnComplete registers a callback fired with the Response when the request finishes, mirroring Typhoeus::Request#on_complete. Callbacks run in registration order.

func (*Request) Response

func (r *Request) Response() *Response

Response returns the Response produced by the last run, or nil if the request has not run yet (Typhoeus::Request#response).

func (*Request) Run

func (r *Request) Run() *Response

Run performs the request through its transport and fires the on-complete callbacks, mirroring Typhoeus::Request#run. It returns the Response (also available via Request.Response).

type Response

type Response struct {
	// Code is the HTTP status code (Response#code / #response_code); 0 when the
	// transfer never produced a status (a connection/timeout failure).
	Code int
	// Body is the response body (Response#body).
	Body string
	// Headers are the response headers (Response#headers).
	Headers *Headers
	// TotalTime is the wall-clock duration of the transfer in seconds
	// (Response#total_time).
	TotalTime float64
	// ReturnCode is the libcurl return code (Response#return_code): [ReturnOK] on
	// success, otherwise the failure mode.
	ReturnCode ReturnCode
	// contains filtered or unexported fields
}

Response is the result of a transfer, mirroring Typhoeus::Response. A Response is always produced — even for a failed transfer, where Code is 0 and ReturnCode names the libcurl failure (see ReturnCode).

func Delete

func Delete(url string, opts ...Options) *Response

Delete performs a one-shot DELETE, mirroring Typhoeus.delete(url, options).

func Get

func Get(url string, opts ...Options) *Response

Get performs a one-shot GET, mirroring Typhoeus.get(url, options). It builds a Request, runs it through DefaultTransport (firing any callbacks the request would carry), and returns the Response.

func Head(url string, opts ...Options) *Response

Head performs a one-shot HEAD, mirroring Typhoeus.head(url, options).

func Patch

func Patch(url string, opts ...Options) *Response

Patch performs a one-shot PATCH, mirroring Typhoeus.patch(url, options).

func Post

func Post(url string, opts ...Options) *Response

Post performs a one-shot POST, mirroring Typhoeus.post(url, options).

func Put

func Put(url string, opts ...Options) *Response

Put performs a one-shot PUT, mirroring Typhoeus.put(url, options).

func (*Response) Request

func (r *Response) Request() *Request

Request returns the Request that produced this response (Typhoeus::Response#request).

func (*Response) ReturnMessage

func (r *Response) ReturnMessage() string

ReturnMessage returns the human-readable return-code name (Response#return_message), e.g. "ok" or "couldnt_connect".

func (*Response) Success

func (r *Response) Success() bool

Success reports whether the transfer succeeded with a 2xx status, mirroring Typhoeus::Response#success? (return_code == :ok and a 200..299 code).

func (*Response) TimedOut

func (r *Response) TimedOut() bool

TimedOut reports whether the transfer timed out, mirroring Typhoeus::Response#timed_out? (return_code == :operation_timedout).

type ReturnCode

type ReturnCode int

ReturnCode is the libcurl transfer result Typhoeus exposes as Response#return_code (a Ruby Symbol such as :ok or :operation_timedout). A successful transfer is ReturnOK; every failure mode maps to a specific code, so a failed request still yields a Response (with Code 0) rather than an error — mirroring libcurl, where failures are return codes, not exceptions.

const (
	// ReturnOK is a completed transfer (CURLE_OK, :ok).
	ReturnOK ReturnCode = iota
	// ReturnCouldntResolveHost is a DNS resolution failure
	// (CURLE_COULDNT_RESOLVE_HOST, :couldnt_resolve_host).
	ReturnCouldntResolveHost
	// ReturnCouldntConnect is a connection failure
	// (CURLE_COULDNT_CONNECT, :couldnt_connect).
	ReturnCouldntConnect
	// ReturnOperationTimedout is a timeout (CURLE_OPERATION_TIMEDOUT,
	// :operation_timedout).
	ReturnOperationTimedout
	// ReturnRecvError is a failure receiving the response body
	// (CURLE_RECV_ERROR, :recv_error).
	ReturnRecvError
	// ReturnInternalError is a request that could not be built at all
	// (an invalid method or URL); Typhoeus reports such a handle as
	// :internal_error.
	ReturnInternalError
)

The return codes modelled here, named after the libcurl CURLcode symbols Typhoeus surfaces.

func (ReturnCode) String

func (c ReturnCode) String() string

String returns the libcurl symbol name for the code (e.g. "operation_timedout"), matching the Symbol Typhoeus returns from Response#return_code. An unknown code renders as "unknown".

type Transport

type Transport interface {
	Run(req *Request) *Response
}

Transport is the host seam that performs a request's HTTP round-trip, the role libcurl plays for Typhoeus via Ethon. Given a Request, a Transport performs the transfer and returns the Response — including the failure case, which is reported as a Response with a non-OK ReturnCode rather than an error, exactly as libcurl reports a CURLcode.

The default production Transport is NetHTTP, backed by net/http. The model opens no socket itself: every request runs through whatever Transport is set, so tests inject a stub (see TransportFunc) and the deterministic suite — the Hydra parallelism included — never touches the network.

var DefaultTransport Transport = NetHTTP()

DefaultTransport is the Transport used by the one-shot verb helpers and by any Request whose Transport field is nil. It defaults to NetHTTP; a host or test may replace it to route every request through a custom transport.

type TransportFunc

type TransportFunc func(req *Request) *Response

TransportFunc adapts a function to the Transport interface, the convenient way to inject a stub transport in tests or a custom transport in a host.

func (TransportFunc) Run

func (f TransportFunc) Run(req *Request) *Response

Run invokes f(req).

Jump to

Keyboard shortcuts

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