diff --git a/backend/internal/apierr/errors.go b/backend/internal/apierr/errors.go new file mode 100644 index 0000000..d620221 --- /dev/null +++ b/backend/internal/apierr/errors.go @@ -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) +} diff --git a/backend/internal/apierr/errors_test.go b/backend/internal/apierr/errors_test.go new file mode 100644 index 0000000..fc0e045 --- /dev/null +++ b/backend/internal/apierr/errors_test.go @@ -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) + } +}