feat(apierr): add UpstreamError type for typed upstream failures

This commit is contained in:
Noooste
2026-04-19 23:59:54 +02:00
parent adfd044798
commit 5c2e5dbf6f
2 changed files with 63 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
// Package apierr translates upstream (Garage Admin API, S3/MinIO) errors into
// backend API error responses with correct HTTP status codes and stable codes.
package apierr
import "fmt"
// UpstreamError is a typed error describing a failure reported by an upstream
// service. Returned from the services layer and consumed by handlers via Map
// or Respond.
type UpstreamError struct {
HTTPStatus int
Code string // upstream code, e.g. "BucketNotEmpty", "NoSuchKey"
Message string // human-readable upstream message
Source string // "garage" or "s3"
Details map[string]string // optional: region, path, bucket, key
}
func (e *UpstreamError) Error() string {
if e.Code == "" {
return fmt.Sprintf("%s: %s", e.Source, e.Message)
}
return fmt.Sprintf("%s %s: %s", e.Source, e.Code, e.Message)
}
+40
View File
@@ -0,0 +1,40 @@
package apierr
import (
"errors"
"testing"
)
func TestUpstreamError_Error(t *testing.T) {
e := &UpstreamError{
HTTPStatus: 409,
Code: "BucketNotEmpty",
Message: "Tried to delete a non-empty bucket",
Source: "garage",
}
got := e.Error()
want := "garage BucketNotEmpty: Tried to delete a non-empty bucket"
if got != want {
t.Fatalf("Error() = %q, want %q", got, want)
}
}
func TestUpstreamError_ErrorWithoutCode(t *testing.T) {
e := &UpstreamError{HTTPStatus: 500, Source: "garage", Message: "server went boom"}
got := e.Error()
want := "garage: server went boom"
if got != want {
t.Fatalf("Error() = %q, want %q", got, want)
}
}
func TestUpstreamError_ErrorsAs(t *testing.T) {
var base error = &UpstreamError{HTTPStatus: 404, Code: "NoSuchBucket", Source: "garage"}
var target *UpstreamError
if !errors.As(base, &target) {
t.Fatal("errors.As should have matched *UpstreamError")
}
if target.Code != "NoSuchBucket" {
t.Fatalf("Code = %q, want NoSuchBucket", target.Code)
}
}