A small network “service” layer that abstracts request execution behind a common interface. It provides:
- A NetSvc facade to execute requests through registered clients
- A default HTTP client
- An S3 client (get/put/list/delete) with middleware support
- Retry behavior with pluggable delay strategies
- A file downloader with progress callbacks and optional SHA-256 verification
- holds global network state/config (headers, timeouts, download options)
- maintains a registry of clients (
ref→ client) - executes
RequestOnce/RequestWithRetryby dispatching to the correct client - publishes transfer notifications for downloads
ClientRefselects the registered client (default:dto.NET_DEFAULT_CLIENT_REF)ReqConfigis a client-specific request spec implementingdto.ReqConfigInterfaceTimeoutapplies a context timeout per callMaxRetries+Delaycontrol retry behaviorResponseObjectoptionally unmarshals JSON into a provided struct
go get github.com/joy-dx/goneticpackage main
import (
"context"
"fmt"
"time"
"github.com/joy-dx/gonetic"
"github.com/joy-dx/gonetic/config"
"github.com/joy-dx/gonetic/dto"
relayDTO "github.com/joy-dx/relay/dto"
)
func main() {
ctx := context.Background()
var relay relayDTO.RelayInterface = /* your relay impl */
netCfg := config.DefaultNetSvcConfig().
WithRelay(relay).
WithPreferCurl(false) // optional
svc := gonetic.ProvideNetSvc(&netCfg)
if err := svc.Hydrate(ctx); err != nil {
panic(err)
}
resp, err := svc.Get(ctx, "https://api.github.com", true)
if err != nil {
panic(err)
}
fmt.Println("status:", resp.StatusCode)
fmt.Println("bytes:", len(resp.Body))
}The main service comes with the following options
ExtraHeaders dto.ExtraHeaders
RequestTimeout time.Duration
UserAgent string
BlacklistDomains []string
WhitelistDomains []string
DownloadCallbackInterval time.Duration
PreferCurlDownloads boolDownloadCallbackInterval: 2sPreferCurlDownloads: false
AuthProvider dto.AuthProvider
OAuthSource oauth2.TokenSource
RefreshBuffer time.Duration
Middlewares []MiddlewareMethod string
URL string
Body map[string]interface{}
BodyType string // application/json or application/x-www-form-urlencoded
Headers map[string]stringMethod:GETBodyType:application/json
httpclient.StaticHeaderMiddleware(map[string]string{
"X-App-Version": "1.2.3",
})httpclient.LoggingMiddleware(func(msg string) {
fmt.Println(msg)
})
// logs: [HTTP] GET https://...httpclient.InjectFieldMiddleware("tenant_id", "t-123")Region string
Credentials aws.CredentialsProvider
Middlewares []Middleware
ForcePathStyle bool
Endpoint string // optional custom endpoints3client.StaticS3MetaMiddleware(map[string]string{
"owner": "platform-team",
})s3client.LoggingMiddleware(func(msg string) {
fmt.Println(msg)
})resp, err := svc.Get(ctx, "https://example.com", true)
resp, err := svc.Post(ctx, "https://example.com", map[string]any{"a": 1}, true)RequestOnce performs:
- validation (
ClientRef,ReqConfig) - client lookup from registry
- type-safety check:
netClient.Type() == cfg.ReqConfig.Ref() - optional per-call timeout via
context.WithTimeout - dispatch to
netClient.ProcessRequest - optional JSON unmarshal into
cfg.ResponseObject
RequestWithRetry retries failures for reliability:
- retries up to
MaxRetries(attempts =MaxRetries + 1) - uses
cfg.Delay.Wait(cfg.TaskName, attempt)between attempts (for attempt > 0) - treats errors as transient if
utils.IsTemporaryErr(err)returns true- current implementation is permissive: if it can’t prove otherwise, it returns
true
- current implementation is permissive: if it can’t prove otherwise, it returns
- retries on HTTP
>= 500responses as server errors
If retries are exhausted on 5xx:
- it returns the last
dto.Responseplus an error indicating attempts were exhausted
utils.ConstantDelay{Period: 1} // secondsutils.ExponentialBackoff{}Default request config uses:
Timeout: 20sMaxRetries: 3Delay:utils.ExponentialBackoff{}
NetSvc supports downloading to a destination folder with progress notifications.
cfg := &dto.DownloadFileConfig{
URL: "https://host/path/file.zip",
DestinationFolder: "/tmp/downloads",
OutputFileName: "file.zip", // optional; derived from URL if empty
Checksum: "", // optional sha256 hex
}
err := svc.DownloadFile(ctx, cfg)NetSvcConfig.PreferCurlDownloads controls the download engine:
- If
PreferCurlDownloads == true, NetSvc executes:
curl -L --progress-bar -o <destination> <url>- Otherwise it streams using
net/httpandio.CopyBuffer
Special behavior:
- On macOS,
Hydrate()forces curl preference to align with download security policy. - If curl is preferred but missing from
$PATH, it falls back tonet/http.
Both download paths publish dto.TransferNotification updates (in-progress, stopped, error, complete).
You can subscribe by URL:
ch, unsub := svc.TransferListener(url)
defer unsub()
go func() {
for n := range ch {
// n.Percentage, n.Downloaded, n.TotalSize, n.Status
}
}()To force-close all listeners for a URL:
svc.TransferListenerClose(url)Notes:
downloadFileWithHTTPreports downloaded/total/percentage periodically (interval fromDownloadCallbackInterval)downloadFileWithCurlparses percentage from curl’s progress output and publishes percentage updates
type GitHubResp struct {
CurrentUserURL string `json:"current_user_url"`
}
httpCfg := httpclient.DefaultHTTPRequestConfig().
WithURL("https://api.github.com")
var out GitHubResp
req := dto.DefaultRequestConfig().
WithClientRef(dto.NET_DEFAULT_CLIENT_REF).
WithReqConfig(&httpCfg).
WithResponseObject(&out).
WithTaskName("github root").
WithTimeout(10 * time.Second).
WithMaxRetries(2).
WithDelay(utils.ExponentialBackoff{})
resp, err := svc.RequestWithRetry(ctx, req)
if err != nil {
// resp may still be useful on 5xx exhaustion
panic(err)
}
fmt.Println(resp.StatusCode, out.CurrentUserURL)You create the HTTP client instance (constructor not shown in artefacts, but Hydrate() uses httpclient.NewHTTPClient(ref, netCfg, httpClientCfg)), then register it:
hcCfg := httpclient.DefaultHTTPClientConfig().
WithMiddleware(
httpclient.LoggingMiddleware(log.Println),
httpclient.StaticHeaderMiddleware(map[string]string{
"X-Tenant": "t-123",
}),
)
custom := httpclient.NewHTTPClient("custom-http", svcCfg, &hcCfg)
svc.RegisterClient("custom-http", custom)Then send a request with ClientRef: "custom-http".
url := "https://example.com/big.tar.gz"
ch, unsub := svc.TransferListener(url)
defer unsub()
go func() {
for n := range ch {
fmt.Printf("%s %.1f%%\n", n.Status, n.Percentage)
}
}()
err := svc.DownloadFile(ctx, &dto.DownloadFileConfig{
URL: url,
DestinationFolder: "/tmp",
})
if err != nil {
panic(err)
}