fetch

package module
v0.0.1-beta.7 Latest Latest
Warning

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

Go to latest
Published: Jan 14, 2026 License: MIT Imports: 16 Imported by: 0

README

go-fetch

Composable HTTP client built on pure net/http with middleware support.

Philosophy

Less is more. Zero non-standard dependencies, no magic, explicit errors.

  • Pure net/http - standard library only
  • Middleware composition - clean separation of concerns
  • Explicit error handling - never ignore, always wrap
  • TDD with table-driven tests

Installation

go get github.com/rockcookies/go-fetch

Quick Start

dispatcher := fetch.NewDispatcher(nil)

resp := dispatcher.R().
    JSON(map[string]string{"name": "John"}).
    Send("POST", "https://api.example.com/users")
defer resp.Close()

if resp.Error != nil {
    return fmt.Errorf("request failed: %w", resp.Error)
}

var user User
if err := resp.JSON(&user); err != nil {
    return fmt.Errorf("decode failed: %w", err)
}

Core Concepts

Dispatcher

Wraps http.Client with middleware chains:

// Default client
dispatcher := fetch.NewDispatcher(nil)

// Custom client
client := &http.Client{Timeout: 10 * time.Second}
dispatcher := fetch.NewDispatcher(client)

// Global middleware
dispatcher.Use(authMiddleware)
Request

Chain-able request builder:

resp := dispatcher.R().
    HeaderKV("Authorization", "Bearer token").
    AddCookie(&http.Cookie{Name: "session", Value: "abc"}).
    JSON(payload).
    Send("POST", url)
defer resp.Close()
Middleware

Standard net/http handler pattern:

func authMiddleware(next fetch.Handler) fetch.Handler {
    return fetch.HandlerFunc(func(client *http.Client, req *http.Request) (*http.Response, error) {
        req.Header.Set("Authorization", "Bearer "+getToken())
        return next.Handle(client, req)
    })
}

API Reference

Request Body
// JSON/XML
req.JSON(data)              // encoding/json
req.XML(data)               // encoding/xml

// Form
req.Form(url.Values{})      // application/x-www-form-urlencoded

// Raw
req.Body(reader)            // io.Reader
req.BodyGet(func() (io.Reader, error) {...})     // lazy evaluation
req.BodyGetBytes(func() ([]byte, error) {...})   // lazy bytes

// Multipart
req.Multipart([]*fetch.MultipartField{...})
Headers & Cookies
// Headers
req.HeaderKV("Content-Type", "application/json")   // set single
req.AddHeaderKV("Accept", "text/html")             // append single
req.HeaderFromMap(map[string]string{...})          // set batch
req.Header(func(h http.Header) {...})              // custom logic

// Cookies
req.AddCookie(&http.Cookie{...})    // append
req.DelAllCookies()                 // clear
Response
resp := req.Send("GET", url)
defer resp.Close()  // ALWAYS defer

if resp.Error != nil {
    return fmt.Errorf("failed: %w", resp.Error)  // NEVER ignore
}

// Access
statusCode := resp.RawResponse.StatusCode
body := resp.String()
bytes := resp.Bytes()

// Decode
var result T
if err := resp.JSON(&result); err != nil {
    return fmt.Errorf("decode: %w", err)
}
Client Tuning
dispatcher.Use(fetch.ClientFuncs(func(c *http.Client) {
    c.Timeout = 5 * time.Second
    c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse  // disable redirects
    }
}))

Testing

go test ./...           # all tests
go test -v -race ./...  # with race detector
go test -cover ./...    # coverage

TDD cycle: Red (write failing test) → Green (make it pass) → Refactor.

Contributing

All contributions must follow docs/constitution.md:

  1. YAGNI - implement only what's needed
  2. Standard Library First - net/http over frameworks
  3. Explicit Errors - never _, always wrap with %w
  4. TDD - failing test first, table-driven style
  5. Single Responsibility - one thing, done well

License

MIT - see LICENSE.

Documentation

Overview

Package fetch provides a flexible and composable HTTP client with middleware support.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Dispatcher

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

Dispatcher manages HTTP client operations with middleware support. It wraps an http.Client and applies middleware chains to requests. All methods are safe for concurrent use.

func NewDispatcher

func NewDispatcher(client *http.Client, middlewares ...Middleware) *Dispatcher

NewDispatcher creates a new Dispatcher with the given HTTP client and middleware. If client is nil, a default client is created with no timeout.

func NewDispatcherWithTransport

func NewDispatcherWithTransport(transport http.RoundTripper, middlewares ...Middleware) *Dispatcher

NewDispatcherWithTransport creates a new Dispatcher with a custom RoundTripper transport. A default http.Client with no timeout is created using the provided transport.

func (*Dispatcher) Client

func (d *Dispatcher) Client() *http.Client

Client returns the underlying HTTP client.

func (*Dispatcher) Clone

func (d *Dispatcher) Clone() *Dispatcher

Clone creates a shallow copy of the Dispatcher. The HTTP client is cloned, and middlewares are copied.

func (*Dispatcher) CoreMiddlewares

func (d *Dispatcher) CoreMiddlewares() []Middleware

CoreMiddlewares returns the dispatcher's core middlewares.

func (*Dispatcher) Dispatch

func (d *Dispatcher) Dispatch(req *http.Request, middlewares ...Middleware) (*http.Response, error)

Dispatch executes the HTTP request with the dispatcher's middleware chain. Middleware execution order (outermost to innermost):

  1. d.middlewares (dispatcher's middleware)
  2. middlewares (per-request middleware)
  3. d.coreMiddlewares (core middleware, applied last)

func (*Dispatcher) Middlewares

func (d *Dispatcher) Middlewares() []Middleware

Middlewares returns the current middleware chain.

func (*Dispatcher) NewRequest

func (d *Dispatcher) NewRequest(middlewares ...Middleware) *Request

NewRequest creates a new Request bound to this dispatcher.

func (*Dispatcher) R

func (d *Dispatcher) R(middlewares ...Middleware) *Request

R is an alias for NewRequest, creating a new Request bound to this dispatcher.

func (*Dispatcher) SetClient

func (d *Dispatcher) SetClient(client *http.Client)

SetClient replaces the underlying HTTP client. This operation is safe for concurrent use. If client is nil, the method does nothing.

func (*Dispatcher) SetCoreMiddlewares

func (d *Dispatcher) SetCoreMiddlewares(middlewares ...Middleware)

SetCoreMiddlewares replaces the dispatcher's core middlewares.

func (*Dispatcher) SetMiddlewares

func (d *Dispatcher) SetMiddlewares(middlewares ...Middleware)

SetMiddlewares replaces the current middleware chain.

func (*Dispatcher) Use

func (d *Dispatcher) Use(middlewares ...Middleware)

Use appends middleware to the dispatcher's middleware chain. Note: This modifies the dispatcher in place. If you need an immutable copy, use Clone() first.

func (*Dispatcher) UseCore

func (d *Dispatcher) UseCore(middlewares ...Middleware)

UseCore appends middleware to the dispatcher's core middleware chain. Core middlewares are applied last (innermost layer) in the middleware chain. Note: This modifies the dispatcher in place.

type Handler

type Handler interface {
	Handle(client *http.Client, req *http.Request) (*http.Response, error)
}

Handler executes HTTP requests. Receives both client and request to enable middleware to modify or replace the client if needed.

type HandlerFunc

type HandlerFunc func(client *http.Client, req *http.Request) (*http.Response, error)

HandlerFunc adapts functions to Handler interface.

func (HandlerFunc) Handle

func (h HandlerFunc) Handle(client *http.Client, req *http.Request) (*http.Response, error)

Handle calls the underlying function.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps Handler to add cross-cutting concerns. Can short-circuit the chain or delegate to next handler.

func AddCookie

func AddCookie(cookies ...*http.Cookie) Middleware

AddCookie adds one or more HTTP cookies to the request. Multiple cookies with the same name will all be sent.

func AddHeaderFromMap

func AddHeaderFromMap(headers map[string]string) Middleware

AddHeaderFromMap adds multiple headers from a map. Preserves existing values.

func AddHeaderKV

func AddHeaderKV(key, value string) Middleware

AddHeaderKV adds a header value. Preserves existing values for the same key.

func AddQueryFromMap

func AddQueryFromMap(params map[string]string) Middleware

AddQueryFromMap adds multiple query parameters from a map. Preserves existing values.

func AddQueryKV

func AddQueryKV(key, value string) Middleware

AddQueryKV adds a query parameter. Preserves existing values for the same key.

func DelAllCookies

func DelAllCookies() Middleware

DelAllCookies removes all cookies from the request by deleting the Cookie header.

func DelHeader

func DelHeader(keys ...string) Middleware

DelHeader removes headers by key.

func DelQuery

func DelQuery(keys ...string) Middleware

DelQuery removes query parameters by key.

func SetBaseURL

func SetBaseURL(uri string) Middleware

SetBaseURL returns a middleware that sets the base URL (scheme and host) for the request. If the URI doesn't include a scheme (http:// or https://), it defaults to http://.

This is useful for targeting different environments (dev, staging, prod) or when the base URL needs to be determined dynamically.

Example:

// Both will set the base URL to http://api.example.com
fetch.SetBaseURL("http://api.example.com")
fetch.SetBaseURL("api.example.com")

func SetBody

func SetBody(reader io.Reader) Middleware

SetBody sets the request body from an io.Reader. Note: The reader is consumed and cannot be retried. Use SetBodyGet for retry support.

func SetBodyForm

func SetBodyForm(data url.Values) Middleware

SetBodyForm encodes form data as the request body. Sets Content-Type to application/x-www-form-urlencoded.

func SetBodyGet

func SetBodyGet(getReader func() (io.Reader, error)) Middleware

SetBodyGet lazily provides the request body via a getter function. The getter is called on each request attempt, enabling retry support.

func SetBodyGetBytes

func SetBodyGetBytes(getBytes func() ([]byte, error)) Middleware

SetBodyGetBytes lazily provides the request body as bytes, supporting retries.

func SetBodyJSON

func SetBodyJSON(data any) Middleware

SetBodyJSON marshals data to JSON as the request body. Sets Content-Type to application/json.

func SetBodyXML

func SetBodyXML(data any) Middleware

SetBodyXML marshals data to XML as the request body. Sets Content-Type to application/xml.

func SetContentType

func SetContentType(contentType string) Middleware

SetContentType sets the Content-Type header.

func SetHeader

func SetHeader(funcs ...func(h http.Header)) Middleware

SetHeader applies functions to modify request headers. Functions execute in order. Use this for complex header logic beyond simple key-value pairs.

func SetHeaderFromMap

func SetHeaderFromMap(headers map[string]string) Middleware

SetHeaderFromMap sets multiple headers from a map. Replaces existing values.

func SetHeaderKV

func SetHeaderKV(key, value string) Middleware

SetHeaderKV sets a header value. Replaces existing values for the same key.

func SetMultipart

func SetMultipart(fields []*MultipartField, opts ...func(*MultipartOptions)) Middleware

SetMultipart builds multipart/form-data requests. Streams fields through a pipe to avoid memory overhead. Supports progress tracking.

func SetPathParams

func SetPathParams(params map[string]string) Middleware

SetPathParams returns a middleware that replaces path parameter placeholders with actual values. Placeholders should be in the format {key}, and they will be replaced with the corresponding value from the params map.

This is useful for RESTful APIs with path parameters like /users/{id}/posts/{postId}.

Example:

// Request URL: /users/{id}/posts/{postId}
// After SetPathParams(map[string]string{"id": "123", "postId": "456"})
// Result: /users/123/posts/456

func SetPathPrefix

func SetPathPrefix(prefix string) Middleware

SetPathPrefix returns a middleware that prepends a path segment to the request URL's path. This is useful for adding API base paths or namespace prefixes.

Example:

// Request URL: /users
// After SetPathPrefix("/api/v1"): /api/v1/users

func SetPathSuffix

func SetPathSuffix(suffix string) Middleware

SetPathSuffix returns a middleware that appends a path segment to the request URL's path. This is useful for adding API versions or resource identifiers to the end of a path.

Example:

// Request URL: /api/users
// After SetPathSuffix("/123"): /api/users/123

func SetQuery

func SetQuery(funcs ...func(query url.Values)) Middleware

SetQuery applies functions to modify URL query parameters. Functions execute in order. Use this for complex query logic beyond simple key-value pairs.

func SetQueryFromMap

func SetQueryFromMap(params map[string]string) Middleware

SetQueryFromMap sets multiple query parameters from a map. Replaces existing values.

func SetQueryKV

func SetQueryKV(key, value string) Middleware

SetQueryKV sets a query parameter. Replaces existing values for the same key.

func SetUserAgent

func SetUserAgent(userAgent string) Middleware

SetUserAgent sets the User-Agent header.

func Skip

func Skip() Middleware

Skip returns a no-op middleware that passes requests through unchanged.

type MultipartField

type MultipartField struct {
	Name                    string
	FileName                string
	ContentType             string
	GetReader               func() (io.ReadCloser, error)
	FileSize                int64
	ExtraContentDisposition map[string]string
	ProgressInterval        time.Duration
	ProgressCallback        MultipartFieldCallbackFunc
	Values                  []string
}

MultipartField represents a multipart/form-data field. Supports both simple form values and file uploads with optional progress tracking.

type MultipartFieldCallbackFunc

type MultipartFieldCallbackFunc func(MultipartFieldProgress)

MultipartFieldCallbackFunc receives progress updates during upload.

type MultipartFieldProgress

type MultipartFieldProgress struct {
	Name     string
	FileName string
	FileSize int64
	Written  int64
}

MultipartFieldProgress reports upload progress for a field.

type MultipartOptions

type MultipartOptions struct {
	Boundary string
}

MultipartOptions configures multipart request creation.

type Request

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

Request represents an HTTP request builder that can accumulate middleware before being executed. It maintains a reference to its parent Dispatcher and builds up a middleware chain.

func (*Request) AddCookie

func (r *Request) AddCookie(cookie ...*http.Cookie) *Request

AddCookie appends one or more cookies to the request. Use this when you need to send authentication tokens or session data.

func (*Request) AddHeaderFromMap

func (r *Request) AddHeaderFromMap(headers map[string]string) *Request

AddHeaderFromMap appends multiple headers from a map without replacing existing values. Use this when you have a set of headers to add in batch.

func (*Request) AddHeaderKV

func (r *Request) AddHeaderKV(key, value string) *Request

AddHeaderKV appends a header value to the request without replacing existing values. Use this when a header can have multiple values (e.g., Accept, Cookie).

func (*Request) AddQueryFromMap

func (r *Request) AddQueryFromMap(params map[string]string) *Request

AddQueryFromMap appends multiple query parameters from a map without replacing existing values. Use this when you have a set of parameters to add in batch.

func (*Request) AddQueryKV

func (r *Request) AddQueryKV(key, value string) *Request

AddQueryKV appends a query parameter without replacing existing values. Use this when a parameter can have multiple values (e.g., ?tag=a&tag=b).

func (*Request) BaseURL

func (r *Request) BaseURL(baseURL string) *Request

BaseURL sets the base URL (scheme and host) for the request. Use this to target different environments or when the base URL is dynamic.

func (*Request) Body

func (r *Request) Body(reader io.Reader) *Request

Body sets the request body from an io.Reader. Options can configure Content-Type and automatic Content-Length.

func (*Request) BodyGet

func (r *Request) BodyGet(get func() (io.Reader, error)) *Request

BodyGet sets the request body using a lazy getter function. The function is called when the body is actually needed.

func (*Request) BodyGetBytes

func (r *Request) BodyGetBytes(get func() ([]byte, error)) *Request

BodyGetBytes sets the request body using a lazy getter function that returns bytes. The function is called when the body is actually needed.

func (*Request) Clone

func (r *Request) Clone() *Request

Clone creates a shallow copy of the Request. The dispatcher reference is preserved, and middleware are copied.

func (*Request) ContentType

func (r *Request) ContentType(contentType string) *Request

ContentType sets the Content-Type header. Most body methods (JSON, XML, Form) set this automatically.

func (*Request) DelAllCookies

func (r *Request) DelAllCookies() *Request

DelAllCookies removes all cookies from the request. Use this when you need to ensure no cookies are sent with the request.

func (*Request) DelHeader

func (r *Request) DelHeader(keys ...string) *Request

DelHeader removes one or more headers from the request. Use this when you need to prevent certain headers from being sent.

func (*Request) DelQuery

func (r *Request) DelQuery(keys ...string) *Request

DelQuery removes one or more query parameters from the request. Use this when you need to prevent certain parameters from being sent.

func (*Request) Delete

func (r *Request) Delete(url string) *Response

Delete method does DELETE HTTP request. It's defined in section 9.3.5 of RFC 9110.

func (*Request) DeleteContext

func (r *Request) DeleteContext(ctx context.Context, url string) *Response

DeleteContext performs a DELETE request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Do

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

Do executes the HTTP request with accumulated middleware.

func (*Request) Form

func (r *Request) Form(form url.Values) *Request

Form sets the request body as URL-encoded form data. Automatically sets Content-Type to application/x-www-form-urlencoded.

func (*Request) Get

func (r *Request) Get(url string) *Response

Get method does GET HTTP request. It's defined in section 9.3.1 of RFC 9110.

func (*Request) GetContext

func (r *Request) GetContext(ctx context.Context, url string) *Response

GetContext performs a GET request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Head

func (r *Request) Head(url string) *Response

Head method does HEAD HTTP request. It's defined in section 9.3.2 of RFC 9110.

func (*Request) HeadContext

func (r *Request) HeadContext(ctx context.Context, url string) *Response

HeadContext performs a HEAD request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Header

func (r *Request) Header(funcs ...func(http.Header)) *Request

Header applies one or more functions to modify the request headers. Use this for complex header manipulation that requires custom logic.

func (*Request) HeaderFromMap

func (r *Request) HeaderFromMap(headers map[string]string) *Request

HeaderFromMap sets multiple headers from a map, replacing existing values. Use this when you want to reset headers to a known state.

func (*Request) HeaderKV

func (r *Request) HeaderKV(key, value string) *Request

HeaderKV sets a single header value, replacing any existing values. Use this when you want to ensure only one value for a header (e.g., Content-Type).

func (*Request) JSON

func (r *Request) JSON(data any) *Request

JSON sets the request body as JSON-encoded data. Accepts string, []byte, or any type that can be marshaled to JSON. Automatically sets Content-Type to application/json.

func (*Request) Multipart

func (r *Request) Multipart(fields []*MultipartField, opts ...func(*MultipartOptions)) *Request

Multipart creates a multipart/form-data request body with the given fields.

func (*Request) Options

func (r *Request) Options(url string) *Response

Options method does OPTIONS HTTP request. It's defined in section 9.3.7 of RFC 9110.

func (*Request) OptionsContext

func (r *Request) OptionsContext(ctx context.Context, url string) *Response

OptionsContext performs an OPTIONS request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Patch

func (r *Request) Patch(url string) *Response

Patch method does PATCH HTTP request. It's defined in section 2 of RFC 5789.

func (*Request) PatchContext

func (r *Request) PatchContext(ctx context.Context, url string) *Response

PatchContext performs a PATCH request with the given context. Use the context to control timeouts and cancellation.

func (*Request) PathParams

func (r *Request) PathParams(params map[string]string) *Request

PathParams replaces path parameter placeholders with actual values. Use this for RESTful APIs with path parameters (e.g., /users/{id} → /users/123).

func (*Request) PathPrefix

func (r *Request) PathPrefix(prefix string) *Request

PathPrefix prepends a path segment to the request URL's path. Use this to add API base paths (e.g., /users → /api/v1/users).

func (*Request) PathSuffix

func (r *Request) PathSuffix(suffix string) *Request

PathSuffix appends a path segment to the request URL's path. Use this to add resource identifiers (e.g., /users → /users/123).

func (*Request) Post

func (r *Request) Post(url string) *Response

Post method does POST HTTP request. It's defined in section 9.3.3 of RFC 9110.

func (*Request) PostContext

func (r *Request) PostContext(ctx context.Context, url string) *Response

PostContext performs a POST request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Put

func (r *Request) Put(url string) *Response

Put method does PUT HTTP request. It's defined in section 9.3.4 of RFC 9110.

func (*Request) PutContext

func (r *Request) PutContext(ctx context.Context, url string) *Response

PutContext performs a PUT request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Query

func (r *Request) Query(funcs ...func(url.Values)) *Request

Query applies one or more functions to modify the request query parameters. Use this for complex query manipulation that requires custom logic.

func (*Request) Send

func (r *Request) Send(method string, u string) *Response

Send constructs and executes an HTTP request with the given method and URL. Returns a Response which wraps the http.Response or any error.

func (*Request) SendContext

func (r *Request) SendContext(ctx context.Context, method string, u string) *Response

func (*Request) SetQueryFromMap

func (r *Request) SetQueryFromMap(params map[string]string) *Request

SetQueryFromMap sets multiple query parameters from a map, replacing existing values. Use this when you want to reset query parameters to a known state.

func (*Request) SetQueryKV

func (r *Request) SetQueryKV(key, value string) *Request

SetQueryKV sets a single query parameter, replacing any existing values. Use this when you want to ensure only one value for a parameter.

func (*Request) Trace

func (r *Request) Trace(url string) *Response

Trace method does TRACE HTTP request. It's defined in section 9.3.8 of RFC 9110.

func (*Request) TraceContext

func (r *Request) TraceContext(ctx context.Context, url string) *Response

TraceContext performs a TRACE request with the given context. Use the context to control timeouts and cancellation.

func (*Request) Use

func (r *Request) Use(middlewares ...Middleware) *Request

Use appends middleware to this request's middleware chain. Returns the request for method chaining.

func (*Request) UserAgent

func (r *Request) UserAgent(userAgent string) *Request

UserAgent sets the User-Agent header. Use this to identify your application to the server.

func (*Request) XML

func (r *Request) XML(data any) *Request

XML sets the request body as XML-encoded data. Accepts string, []byte, or any type that can be marshaled to XML. Automatically sets Content-Type to application/xml.

type Response

type Response struct {
	Error       error
	Header      http.Header
	Cookies     []*http.Cookie
	RawRequest  *http.Request
	RawResponse *http.Response
	// contains filtered or unexported fields
}

Response wraps an HTTP response and provides convenient methods for reading and decoding the response body. It implements io.Reader and buffers content for multiple reads.

func (*Response) Bytes

func (r *Response) Bytes() []byte

Bytes returns the response body as a byte slice. Uses internal buffering for efficient multiple reads.

func (*Response) ClearInternalBuffer

func (r *Response) ClearInternalBuffer()

ClearInternalBuffer resets the internal buffer. Does nothing if an error is present.

func (*Response) Close

func (r *Response) Close() error

Close discards any remaining response body and closes it. Safe to call even when Error is present or RawResponse is nil.

func (*Response) JSON

func (r *Response) JSON(userStruct any) error

JSON decodes the response body as JSON into the provided struct.

func (*Response) Read

func (r *Response) Read(p []byte) (n int, err error)

Read implements io.Reader by reading from the underlying response body. Returns an error if the response contains an error.

func (*Response) SaveToFile

func (r *Response) SaveToFile(fileName string) error

SaveToFile writes the response body to a file. Uses internal buffering if available.

func (*Response) String

func (r *Response) String() string

String returns the response body as a string. Uses internal buffering for efficient multiple reads.

func (*Response) XML

func (r *Response) XML(userStruct any) error

XML decodes the response body as XML into the provided struct.

Directories

Path Synopsis
internal
bufferpool
Package bufferpool provides a buffer pool for bytes.Buffer instances.
Package bufferpool provides a buffer pool for bytes.Buffer instances.
pool
Package pool provides internal pool utilities.
Package pool provides internal pool utilities.

Jump to

Keyboard shortcuts

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