remote

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 17 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:   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

Call marshals Request.Body to JSON and decodes a JSON response. Two other body shapes are available for services that need them:

Call Body Non-2xx
Call / Client.JSONRequest JSON-marshalled Body returned as an error, after decoding into response
CallRaw / Client.RawRequest Body bytes sent verbatim (SOAP/XML) returned in the response, not as an error
CallMultipart / Client.MultipartRequest multipart/form-data parts returned as an error, after decoding into response
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.CallMultipart(ctx, "document-registry", remote.MultipartRequest{
    Method: http.MethodPost,
    Path:   "/api/documents/v1",
    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")

// Execute a pre-built *http.Request directly
resp, err := client.Do(req)

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 Client

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

func NewClient

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

func (*Client) Do

func (c *Client) Do(ctx context.Context, method, path string, body io.Reader, extraHeaders map[string]string, retry *RetryConfig) (*http.Response, error)

func (*Client) JSONRequest

func (c *Client) JSONRequest(ctx context.Context, req Request, response interface{}) error

func (*Client) MultipartRequest added in v0.7.0

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

MultipartRequest sends req.Parts as a multipart/form-data body and decodes a JSON response into response (pass nil to discard it).

Error semantics match JSONRequest rather than RawRequest: the body is decoded first and a non-2xx status is then returned as an error, so a caller that passes a response still sees the service's error payload alongside the error. Parts are buffered in memory, which keeps the body replayable across retries — size uploads with that in mind.

func (*Client) RawRequest added in v0.3.0

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

RawRequest sends req.Body verbatim and returns the raw response. Unlike JSONRequest, 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). The response body read is capped at maxRawResponseBytes.

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) CallMultipart added in v0.7.0

func (m *Manager) CallMultipart(ctx context.Context, serviceID string, req MultipartRequest, response any) error

CallMultipart sends a multipart/form-data request (e.g. a JSON payload part alongside file uploads) to a registered service and decodes a JSON response into response. See Client.MultipartRequest: unlike CallRaw, a non-2xx status is returned as an error, after the body has been decoded into response.

func (*Manager) CallRaw added in v0.3.0

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

CallRaw sends a raw-bodied request (e.g. a SOAP/XML envelope) 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) GetClientByURL

func (m *Manager) GetClientByURL(rawURL string) (*Client, string, error)

func (*Manager) ListServices

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

func (*Manager) LoadServices

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

type MultipartRequest added in v0.7.0

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

MultipartRequest bundles the caller-provided parts of an outbound multipart/form-data call. The Content-Type header (including the generated boundary) is set by the client and must not be supplied in Headers.

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 RawRequest added in v0.3.0

type RawRequest struct {
	Method      string
	Path        string
	ContentType string // sent as Content-Type when Body is non-empty
	Body        []byte
	Headers     map[string]string
	Retry       *RetryConfig // If nil, no retries will be performed
}

RawRequest bundles the caller-provided parts of an outbound call whose body is sent verbatim — no JSON marshalling — e.g. a SOAP/XML envelope.

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    any
	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