This module provides a reusable Go client for Syfon APIs.
- Module:
github.com/calypr/syfon/client - Pattern: Vault-style grouped services (
Client.Data(),Client.Index(),Client.Buckets(), etc.) - Current surface:
- Health:
/healthz - Data:
/data/upload,/data/upload/{file_id},/data/upload/bulk,/data/download/{file_id}, multipart endpoints - Index:
/index,/index/{id}, bulk endpoints - Buckets:
/data/buckets,/data/buckets/{bucket},/data/buckets/{bucket}/scopes - Core:
/index/v1/sha256/validity - Metrics:
/index/v1/metrics/*
- Health:
package main
import (
"context"
"errors"
"log"
"github.com/calypr/syfon/apigen/bucketapi"
"github.com/calypr/syfon/apigen/errorapi"
"github.com/calypr/syfon/client/apierror"
syclient "github.com/calypr/syfon/client"
)
func main() {
c, err := syclient.New(
"http://127.0.0.1:8080",
syclient.WithBasicAuth("user", "pass"),
)
if err != nil {
log.Fatal(err)
}
provider := "s3"
region := "us-east-1"
accessKey := "..."
secretKey := "..."
err = c.Buckets().Put(context.Background(), bucketapi.PutBucketRequest{
Bucket: "cbds",
Provider: &provider,
Region: ®ion,
AccessKey: &accessKey,
SecretKey: &secretKey,
Organization: "syfon",
ProjectId: "e2e",
})
if err != nil {
log.Fatal(err)
}
if err := c.Health().Ping(context.Background()); err != nil {
var apiErr *apierror.APIError
if errors.As(err, &apiErr) {
log.Printf("Syfon request %s failed with %s (%d), request ID %s", apiErr.URL, apiErr.Code, apiErr.Status, apiErr.RequestID)
}
if errors.Is(err, errorapi.ErrUnavailable) {
log.Printf("Syfon is temporarily unavailable")
}
log.Fatal(err)
}
}After it successfully reads an HTTP response body, Syfon maps responses with
status 400 or greater to *apierror.APIError. The error preserves the HTTP
status, exact machine-readable code, broad error category, public message,
request ID, request method and URL, response headers, and response body. A
response-body read failure remains an ordinary wrapped error. Use errors.Is
with a broad sentinel for portable control flow:
record, err := c.DRS().GetObject(context.Background(), "object-id")
if errors.Is(err, errorapi.ErrNotFound) {
// The object does not exist or is not visible to this caller.
}APIError.Code has the exported errorapi.ErrorCode type and preserves
unknown server-defined codes. Exact sentinels work with errors.Is, while the
broad sentinel remains available for fallback handling:
switch {
case errors.Is(err, errorapi.ErrObjectChecksumImmutable):
// Do not retry this update with a different checksum.
case errors.Is(err, errorapi.ErrConflict):
// Handle another conflict reason.
}Code comparisons such as
apiErr.Code == errorapi.ErrorCodeObjectChecksumImmutable are also stable.
APIError.Category has the exported errorapi.ErrorCategory type. The
exported broad sentinels are ErrNotFound, ErrUnauthorized, ErrForbidden,
ErrConflict, ErrInvalidInput, ErrRateLimited, and ErrUnavailable.
Import error definitions from github.com/calypr/syfon/apigen/errorapi.
Use an exact sentinel when the operation needs one reason:
if errors.Is(err, errorapi.ErrObjectNotFound) {
// The requested object is missing.
}Use errorapi.ErrNotFound when any not-found reason is enough. An exact
not-found error also matches the broad sentinel. The broad sentinel does not
identify a particular exact reason. The old error constants and type aliases
from client/apierror were removed. client/services.ErrObjectNotFound is
the deprecated compatibility alias; use errorapi.ErrNotFound to preserve its
broad matching behavior. The client/apierror package owns the
APIError response type, while apigen/errorapi owns sentinels and codes.
The client returns ordinary wrapped errors for request construction, transport
failure, response-body reads, and malformed JSON in a successful response. A
malformed non-2xx response, after its body is read, still returns
*apierror.APIError. The parser uses the HTTP status when the response body is
empty or is not valid JSON. For a valid payload, a known exact code from the
top-level or nested error object wins over numeric code or status fields, and
its category is derived from that code. An unknown code string is kept in
APIError.Code for forward compatibility. The parsed message and request ID
are preserved. Fractional and out-of-range numeric codes are ignored.
The server accepts X-Request-Id. If the header is empty, the server creates a
request ID. The server includes that ID in the error response and logs it with
the internal error. Error responses use public-safe status text for 5xx
messages. Inspect APIError.Body or server logs only in a controlled
diagnostic context, because those values can contain provider or database
details. When both the response header and JSON body contain a request ID, the
client uses the response header.
LFSService.Batch and LFSService.StageMetadata require their generated 200
response payloads. A missing success payload is returned as a shared
*apierror.APIError. LFS batch object errors remain in the batch response and
must be checked per object. LFSService.Verify accepts only HTTP 200.
The request layer retries only bodyless GET, HEAD, and OPTIONS calls by
default. Multipart writes are not retried by that layer. The multipart engine
persists a checkpoint after initialization and after each successful part, then
removes it after successful completion. A failed completion leaves the
checkpoint for a later resume. A repeated completion that returns not found is
an error; the client does not infer success from a prior attempt. Server-side
multipart sessions are process-memory state, so a server restart loses the
session lookup. This is separate from the client checkpoint, which can support
resuming a transfer after a client restart.