remote

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

README

remote

Registry-based outbound HTTP client manager. Services (external agencies, backend APIs) are declared in a JSON config file; the manager resolves endpoints, applies authentication, and executes requests — no per-service boilerplate in your application code.

Usage

import "github.com/OpenNSW/core/remote"

manager := remote.NewManager()
if err := manager.LoadServices("configs/services.json"); err != nil {
    log.Fatal(err)
}

// Call a registered service
var result MyResponseType
err := manager.Call(ctx, "npqs-api", remote.Request{
    Method: http.MethodPost,
    Path:   "/v1/applications",
    Body:   remote.JSONBody{V: myPayload},
}, &result)

Services config

services.json declares available services with their endpoint, timeout, and authentication:

{
  "version": "1",
  "services": [
    {
      "id": "npqs-api",
      "url": "https://npqs.example.gov/api",
      "timeout_seconds": 30,
      "auth": {
        "type": "oauth2",
        "options": {
          "token_url": "https://idp.example.gov/token",
          "client_id": "my-client",
          "client_secret": "secret",
          "scopes": ["npqs:submit"]
        }
      }
    },
    {
      "id": "legacy-api",
      "url": "https://legacy.example.gov",
      "timeout_seconds": 10,
      "auth": {
        "type": "api_key",
        "options": {
          "key": "X-API-Key",
          "value": "my-api-key"
        }
      }
    }
  ]
}

Authentication strategies

See remote/auth for the full reference. Supported types:

type Description
api_key Static header (e.g. X-API-Key: value)
bearer Authorization: Bearer <token>
oauth2 Client credentials flow with automatic token caching

Request bodies

Request.Body is a remote.Body — an interface that encodes itself into wire bytes plus the Content-Type header describing them. Built-in implementations:

Body Encodes as
JSONBody{V} JSON-marshalled V
RawBody{Data, ContentType} Data sent verbatim under ContentType (e.g. SOAP/XML)
FormBody{Values} application/x-www-form-urlencoded
MultipartBody{Parts} multipart/form-data

A nil Body sends no request body at all (e.g. a plain GET).

Two calls consume a Request, differing only in how they treat the response:

Call Response handling
Call / Client.Request decodes a JSON response into response; non-2xx is returned as an error, after decoding
CallRaw / Client.RawRequest returns the raw, undecoded response; non-2xx is not an error

MultipartBody decodes and errors the same way JSONBody does, so multipart calls go through Call / Client.Request too — there is no separate multipart call.

multipart/form-data

For services that take a JSON document alongside file uploads. Parts are sent in the order given, which matters for receivers that pair a fileN part with an ordered list inside the payload:

payload, err := remote.JSONPart("payload", application)
if err != nil {
    return err
}

var ack struct {
    ID     string `json:"id"`
    Status string `json:"status"`
}
err = manager.Call(ctx, "document-registry", remote.Request{
    Method: http.MethodPost,
    Path:   "/api/documents/v1",
    Body: remote.MultipartBody{Parts: []remote.Part{
        payload,
        {Name: "fileinfo", Content: []byte("1")},
        {Name: "file1", FileName: "invoice.pdf", ContentType: "application/pdf", Content: pdf},
    }},
}, &ack)

A Part with an empty FileName is sent as a plain form field; setting FileName sends it as an uploaded file. ContentType is written as the part's own Content-Type header when set and omitted otherwise — some receivers tell a JSON part from a text field by that header alone, which is why JSONPart sets it for you.

The request Content-Type (including the generated boundary) is set by the client and cannot be overridden via Headers; supplying one would strip the boundary and leave the body unparseable. Parts are buffered in memory so the body can be replayed across retries — size uploads with that in mind.

Direct client access

client, err := manager.GetClient("npqs-api")

var result MyResponseType
err = client.Request(ctx, remote.Request{Method: http.MethodGet, Path: "/v1/applications/123"}, &result)

Listing registered services

ids := manager.ListServices() // []string{"npqs-api", "legacy-api"}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrRequestFailed      = errors.New("remote: request failed")
	ErrTimeout            = errors.New("remote: request timed out")
	ErrServiceUnavailable = errors.New("remote: service unavailable")
	ErrUnauthorized       = errors.New("remote: unauthorized access")
	ErrBadRequest         = errors.New("remote: invalid request")
	ErrNotFound           = errors.New("remote: resource not found")
)
View Source
var DefaultRetryConfig = RetryConfig{
	MaxRetries:     3,
	InitialBackoff: 500 * time.Millisecond,
	MaxBackoff:     10 * time.Second,
	RetryableStatus: []int{
		http.StatusTooManyRequests,
		http.StatusInternalServerError,
		http.StatusBadGateway,
		http.StatusServiceUnavailable,
		http.StatusGatewayTimeout,
	},
}

DefaultRetryConfig provides a sensible default for most services.

Functions

This section is empty.

Types

type AuthConfig

type AuthConfig struct {
	Type    string          `json:"type"` // "api_key", "oauth2", "bearer"
	Options json.RawMessage `json:"options"`
}

type Body added in v0.8.0

type Body interface {
	Encode() (data []byte, contentType string, err error)
}

Body encodes a request payload into wire bytes and the Content-Type header that describes them. The returned Content-Type always wins over the same key in Request.Headers — the encoding (e.g. a multipart boundary) is only valid paired with the Content-Type that names it.

type Client

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

func NewClient

func NewClient(baseURL string, opts ...Option) *Client

func (*Client) RawRequest added in v0.3.0

func (c *Client) RawRequest(ctx context.Context, req Request) (*RawResponse, error)

RawRequest sends req and returns the raw response, uninterpreted. Unlike Request, a non-2xx status is NOT an error: protocols like SOAP deliver faults as HTTP 500 with a meaningful body, so the caller interprets the status and body together. The returned error is transport-level only (connection, timeout, auth application, body encoding). The response body read is capped at maxRawResponseBytes.

func (*Client) Request added in v0.8.0

func (c *Client) Request(ctx context.Context, req Request, response any) error

Request sends req and decodes a JSON response into response (pass nil to discard it). A non-2xx status is returned as an error, after the body has been decoded into response — see RawRequest for pass-through semantics.

type FormBody added in v0.8.0

type FormBody struct {
	Values url.Values
}

FormBody encodes Values as application/x-www-form-urlencoded.

func (FormBody) Encode added in v0.8.0

func (b FormBody) Encode() ([]byte, string, error)

type JSONBody added in v0.8.0

type JSONBody struct {
	V any
}

JSONBody marshals V as a JSON request body.

func (JSONBody) Encode added in v0.8.0

func (b JSONBody) Encode() ([]byte, string, error)

type Manager

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

func NewManager

func NewManager() *Manager

func (*Manager) Call

func (m *Manager) Call(ctx context.Context, serviceID string, req Request, response interface{}) error

func (*Manager) CallRaw added in v0.3.0

func (m *Manager) CallRaw(ctx context.Context, serviceID string, req Request) (*RawResponse, error)

CallRaw sends a raw-bodied request (e.g. a SOAP/XML envelope, via req.Body = RawBody{...}) to a registered service. See Client.RawRequest for the error semantics: a non-2xx status is returned in the response, not as an error.

func (*Manager) GetClient

func (m *Manager) GetClient(id string) (*Client, error)

func (*Manager) ListServices

func (m *Manager) ListServices() []string

func (*Manager) LoadServices

func (m *Manager) LoadServices(filePath string) error

type MultipartBody added in v0.8.0

type MultipartBody struct {
	Parts []Part
}

MultipartBody encodes Parts as a multipart/form-data request body. Encode generates a fresh boundary on every call, so its Content-Type return must be used verbatim — it cannot be precomputed or cached. Parts are buffered in memory, which keeps the body replayable across retries — size uploads with that in mind. See multipart.go for Part and the encoding itself.

func (MultipartBody) Encode added in v0.8.0

func (b MultipartBody) Encode() ([]byte, string, error)

type Option

type Option func(*Client)

func WithAuthenticator

func WithAuthenticator(a auth.Authenticator) Option

func WithClientCertificate added in v0.3.0

func WithClientCertificate(cert tls.Certificate) Option

WithClientCertificate presents a fixed certificate during the TLS handshake (mTLS). For material that rotates on disk, prefer WithClientCertificateFiles.

func WithClientCertificateFiles added in v0.3.0

func WithClientCertificateFiles(certFile, keyFile string) Option

WithClientCertificateFiles presents the client certificate at certFile / keyFile during the TLS handshake (mTLS). The PEM files are read on each handshake — a per-connection, not per-request, cost — so rotated material is picked up by new connections with no restart (zero-downtime rotation), and a missing or malformed file fails the call with a clear error.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

type Part added in v0.7.0

type Part struct {
	Name        string // form field name; required
	FileName    string // optional; set to send the part as a file
	ContentType string // optional; e.g. "application/json", "application/pdf"
	Content     []byte
}

Part is one part of a multipart/form-data body.

The zero FileName sends a plain form field; a non-empty FileName sends the part as an uploaded file, adding filename="..." to Content-Disposition. ContentType is written as the part's Content-Type header when set, and omitted otherwise — some receivers distinguish a JSON part from a text field by that header alone.

func JSONPart added in v0.7.0

func JSONPart(name string, v any) (Part, error)

JSONPart marshals v and returns a Part carrying it as application/json. It saves callers hand-marshalling the one part that is rarely a plain string, and keeps the Content-Type spelling consistent across services.

type RawBody added in v0.8.0

type RawBody struct {
	Data        []byte
	ContentType string
}

RawBody sends Data verbatim under ContentType — e.g. a SOAP/XML envelope. An empty Data sends no body and sets no Content-Type.

func (RawBody) Encode added in v0.8.0

func (b RawBody) Encode() ([]byte, string, error)

type RawResponse added in v0.3.0

type RawResponse struct {
	StatusCode int
	Header     http.Header
	Body       []byte
}

RawResponse is the undecoded outcome of a RawRequest.

type Registry

type Registry struct {
	Version  string          `json:"version"`
	Services []ServiceConfig `json:"services"`
}

type RemoteError

type RemoteError struct {
	StatusCode int
	Message    string
	Wrapped    error
}

func (*RemoteError) Error

func (e *RemoteError) Error() string

func (*RemoteError) Unwrap

func (e *RemoteError) Unwrap() error

type Request

type Request struct {
	Method  string
	Path    string
	Query   url.Values
	Body    Body
	Headers map[string]string
	Retry   *RetryConfig // If nil, no retries will be performed
}

Request bundles all the caller-provided parts of an outbound call.

type RetryConfig

type RetryConfig struct {
	MaxRetries      int           // Maximum number of retries (0 = no retries)
	InitialBackoff  time.Duration // Time to wait before the first retry
	MaxBackoff      time.Duration // Maximum wait time between retries
	RetryableStatus []int         // HTTP status codes that should trigger a retry
}

RetryConfig defines the strategy for retrying failed requests.

type ServiceConfig

type ServiceConfig struct {
	ID      string       `json:"id"`
	URL     string       `json:"url"`
	Timeout string       `json:"timeout"`
	Auth    *AuthConfig  `json:"auth,omitempty"`
	TLS     *TLSSettings `json:"tls,omitempty"`
}

type TLSSettings added in v0.3.0

type TLSSettings struct {
	ClientCertFile string `json:"client_cert_file"`
	ClientKeyFile  string `json:"client_key_file"`
}

TLSSettings configures transport-level client authentication (mTLS) for a service. Both values are filesystem paths to PEM files, not secret references: certificate chains routinely exceed the 4 KB cap that secret.SecretRef places on file-sourced secrets.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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