Reggie
Simple Go HTTP client for OCI Distribution, built on top of Resty
Install / Use
/learn @bloodorangeio/ReggieREADME
Reggie

Reggie is a dead simple Go HTTP client designed to be used against OCI Distribution, built on top of Resty.
There is also built-in support for both basic auth and "Docker-style" token auth.
Note: Authentication/authorization is not part of the distribution spec, but it has been implemented similarly across registry providers targeting the Docker client.
Getting Started
First import the library:
import "github.com/bloodorangeio/reggie"
Then construct a client:
client, err := reggie.NewClient("http://localhost:5000")
You may also construct the client with a number of options related to authentication, etc:
client, err := reggie.NewClient("https://r.mysite.io",
reggie.WithUsernamePassword("myuser", "mypass"), // registry credentials
reggie.WIthDefaultName("myorg/myrepo"), // default repo name
reggie.WithInsecureSkipTLSVerify(true), // skip TLS verification
reggie.WithDebug(true)) // enable debug logging
Making Requests
Reggie uses a domain-specific language to supply various parts of the URI path in order to provide visual parity with the spec.
For example, to list all tags for the repo megacorp/superapp, you might do the following:
req := client.NewRequest(reggie.GET, "/v2/<name>/tags/list",
reggie.WithName("megacorp/superapp"))
This will result in a request object built for GET /v2/megacorp/superapp/tags/list.
Finally, execute the request, which will return a response object:
resp, err := client.Do(req)
fmt.Println("Status Code:", resp.StatusCode())
Path Substitutions
Below is a table of all of the possible URI parameter substitutions and associated methods:
| URI Parameter | Description | Option method |
|-|-|-|
| <name> | Namespace of a repository within a registry | WithDefaultName (Client) or<br>WithName (Request) |
| <digest> | Content-addressable identifier | WithDigest (Request) |
| <reference> | Tag or digest | WithReference (Request) |
| <session_id> | Session ID for upload | WithSessionID (Request) |
Auth
All requests are first attempted without any authentication. If an endpoint returns a 401 Unauthorized, and the client has been constructed with a username and password (via reggie.WithUsernamePassword), the request is retried with an Authorization header.
Included in the 401 response, registries should return a Www-Authenticate header describing how to to authenticate.
For more info about the Www-Authenticate header and general HTTP auth topics, please see IETF RFCs 7235 and 6749.
Basic Auth
If the Www-Authenticate header contains the string "Basic", then the header used in the retried request will be formatted as Authorization: Basic <credentials>, where credentials is the base64 encoding of the username and password joined by a single colon.
"Docker-style" Token Auth
Note: most commercial registries use this method.
If theWww-Authenticate contains the string "Bearer", an attempt is made to retrieve a token from an authorization service endpoint, the URL of which should be provided in the Realm field of the header. The header then used in the retried request will be formatted as Authorization: Bearer <token>, where token is the one returned from the token endpoint.
Here is a visual of this auth flow copied from the Docker docs:

Custom Auth Scope
It may be necessary to override the scope obtained from the Www-Authenticate header in the registry's response. This can be done on the client level:
client, err := reggie.NewClient("http://localhost:5000",
reggie.WithAuthScope("repository:mystuff/myrepo:pull,push"))
Other Features
Method Chaining
Each of the types provided by this package (Client, Request, & Response) are all built on top of types provided by Resty. In most cases, methods provided by Resty should just work on these objects (see the godoc for more info).
The following commonly-used methods have been wrapped in order to allow for method chaining:
req.Headerreq.SetQueryParamreq.SetBody
The following is an example of using method chaining to build a request:
req := client.NewRequest(reggie.PUT, lastResponse.GetRelativeLocation()).
SetHeader("Content-Length", configContentLength).
SetHeader("Content-Type", "application/octet-stream").
SetQueryParam("digest", configDigest).
SetBody(configContent)
Location Header Parsing
For certain types of requests, such as chunked uploads, the Location header is needed in order to make follow-up requests.
Reggie provides two helper methods to obtain the redirect location:
fmt.Println("Relative location:", resp.GetRelativeLocation()) // /v2/...
fmt.Println("Absolute location:", resp.GetAbsoluteLocation()) // https://...
Error Parsing
On the response object, you may call the Errors() method which will attempt to parse the response body into a list of OCI ErrorInfo objects:
for _, e := range resp.Errors() {
fmt.Println("Code:", e.Code)
fmt.Println("Message:", e.Message)
fmt.Println("Detail:", e.Detail)
}
HTTP Method Constants
Simply-named constants are provided for the following HTTP request methods:
reggie.GET // "GET"
reggie.PUT // "PUT"
reggie.PATCH // "PATCH"
reggie.DELETE // "DELETE"
reggie.POST // "POST"
reggie.HEAD // "HEAD"
reggie.OPTIONS // "OPTIONS"
Custom User-Agent
By default, requests made by Reggie will use a default value for the User-Agent header in order for registry providers to identify incoming requests:
User-Agent: reggie/0.3.0 (https://github.com/bloodorangeio/reggie)
If you wish to use a custom value for User-Agent, such as "my-agent" for example, you can do the following:
client, err := reggie.NewClient("http://localhost:5000",
reggie.WithUserAgent("my-agent"))
Example
The following is an example of a resumable blob upload and subsequent manifest upload:
package main
import (
"fmt"
"github.com/bloodorangeio/reggie"
godigest "github.com/opencontainers/go-digest"
)
func main() {
// construct client pointing to your registry
client, err := reggie.NewClient("http://localhost:5000",
reggie.WithDefaultName("myorg/myrepo"),
reggie.WithDebug(true))
if err != nil {
panic(err)
}
// get the session URL
req := client.NewRequest(reggie.POST, "/v2/<name>/blobs/uploads/")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
// a blob for an empty manifest config, separated into 2 chunks ("{" and "}")
blob := []byte("{}")
blobChunk1 := blob[:1]
blobChunk1Range := fmt.Sprintf("0-%d", len(blobChunk1)-1)
blobChunk2 := blob[1:]
blobChunk2Range := fmt.Sprintf("%d-%d", len(blobChunk1), len(blob)-1)
blobDigest := godigest.FromBytes(blob).String()
// upload the first chunk
req = client.NewRequest(reggie.PATCH, resp.GetRelativeLocation()).
SetHeader("Content-Type", "application/octet-stream").
SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk1))).
SetHeader("Content-Range", blobChunk1Range).
SetBody(blobChunk1)
resp, err = client.Do(req)
if err != nil {
panic(err)
}
// upload the final chunk and close the session
req = client.NewRequest(reggie.PUT, resp.GetRelativeLocation()).
SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk2))).
SetHeader("Content-Range", blobChunk2Range).
SetHeader("Content-Type", "application/octet-stream").
SetQueryParam("digest", blobDigest).
SetBody(blobChunk2)
resp, err = client.Do(req)
if err != nil {
panic(err)
}
// validate the uploaded blob content
req = client.NewRequest(reggie.GET, "/v2/<name>/blobs/<digest>",
reggie.WithDigest(blobDigest))
resp, err = client.Do(req)
if err != nil {
panic(err)
}
fmt.Printf("Blob content:\n%s\n", resp.String())
// upload the manifest (referencing the uploaded blob)
ref := "mytag"
manifest := []byte(fmt.Sprintf(
"{ \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\", \"config\": { \"digest\": \"%s\", "+
"\"mediaType\": \"application/vnd.oci.image.config.v1+json\","+" \"size\": %d }, \"layers\": [], "+
"\"schemaVersion\": 2 }",
blobDigest, len(blob)))
req = client.NewRequest(reggie.PUT, "/v2/<name>/manifests/<reference>",
reggie.WithReference(ref)).
SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(manifest)
resp, err = client.Do(req)
if err != nil {
panic(err)
}
// validate the uploaded manifest content
req = client.NewRequest(reggie.GET, "/v2/<name>/manifests/<reference>",
reggie.WithReference(ref)).
SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json")
resp, err = client.Do(req)
if err != nil {
panic(err)
}
fmt.Printf("Manifest content:\n%s\n", resp.String())
}
Development
To develop bloodorangeio/reggie, you will need to have Go installed. You should then fork the repository, clone the fork, and checkout a new branch.
git clone https://github.com/<username>/reggie
cd reggie
git checkout -b add/m
Related Skills
node-connect
352.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
xurl
352.2kA CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.
prose
352.2kOpenProse VM skill pack. Activate on any `prose` command, .prose files, or OpenProse mentions; orchestrates multi-agent workflows.
frontend-design
111.1kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
