feat: add project-owned error catalogs and HTTP 200 failure contracts
This commit is contained in:
@@ -45,9 +45,17 @@ func WrapMiddleware(middleware func(http.Handler) http.Handler) Middleware {
|
||||
child.writer.(interface{ inheritCommit(int) }).inheritCommit(c.Status())
|
||||
}
|
||||
defer CleanupMultipart(r)
|
||||
defer func() { c.values = child.values; c.encoder = child.encoder }()
|
||||
if err := next(child); err != nil && !c.Responded() {
|
||||
child.WriteError(err)
|
||||
defer func() {
|
||||
c.values = child.values
|
||||
c.encoder = child.encoder
|
||||
c.errorRegistry = child.errorRegistry
|
||||
c.failure = child.failure
|
||||
}()
|
||||
if err := next(child); err != nil {
|
||||
child.RecordError(err)
|
||||
if !c.Responded() {
|
||||
child.WriteError(err)
|
||||
}
|
||||
}
|
||||
}))
|
||||
if wrapped == nil {
|
||||
|
||||
@@ -88,13 +88,15 @@ func RoutePattern(ctx context.Context) string {
|
||||
// only standard-library HTTP types, so it remains usable when the network
|
||||
// implementation is replaced.
|
||||
type Context struct {
|
||||
request *http.Request
|
||||
writer http.ResponseWriter
|
||||
params map[string]string
|
||||
values map[string]any
|
||||
responded bool
|
||||
status int
|
||||
encoder response.Encoder
|
||||
request *http.Request
|
||||
writer http.ResponseWriter
|
||||
params map[string]string
|
||||
values map[string]any
|
||||
responded bool
|
||||
status int
|
||||
encoder response.Encoder
|
||||
errorRegistry *apierrors.Registry
|
||||
failure *apierrors.APIError
|
||||
}
|
||||
|
||||
// NewContext creates a transport-neutral GoOmni context.
|
||||
@@ -133,6 +135,7 @@ func (c *Context) Raw() http.ResponseWriter { return c.writer }
|
||||
func (c *Context) Clone(ctx context.Context, writer http.ResponseWriter) *Context {
|
||||
clone := NewContext(c.request.Clone(ctx), writer, c.params)
|
||||
clone.encoder = c.encoder
|
||||
clone.errorRegistry = c.errorRegistry
|
||||
for key, value := range c.values {
|
||||
clone.values[key] = value
|
||||
}
|
||||
@@ -279,15 +282,43 @@ func (c *Context) WriteError(err error) {
|
||||
if errors.As(err, &bodyErr) {
|
||||
err = bodyTooLarge()
|
||||
}
|
||||
apiErr := apierrors.From(err)
|
||||
apiErr := c.ResolveError(err)
|
||||
c.failure = apiErr
|
||||
if writeErr := c.writeResult(response.Result{
|
||||
Status: apiErr.Status,
|
||||
Code: apiErr.Code,
|
||||
Message: apiErr.Message,
|
||||
RequestID: c.RequestID(),
|
||||
Err: err,
|
||||
BusinessCode: apiErr.BusinessCode,
|
||||
Failed: true,
|
||||
Status: apiErr.Status,
|
||||
Code: apiErr.Code,
|
||||
Message: apiErr.Message,
|
||||
RequestID: c.RequestID(),
|
||||
Err: err,
|
||||
}); writeErr != nil && !c.responded {
|
||||
// An invalid custom encoder must not recurse or leak a different envelope.
|
||||
_ = c.WriteResponse(http.StatusInternalServerError, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// SetErrorRegistry sets an application fallback. Route encoders can supply their
|
||||
// own registry; clones retain both policies without any process-global lookup.
|
||||
func (c *Context) SetErrorRegistry(r *apierrors.Registry) { c.errorRegistry = r }
|
||||
func (c *Context) ResolveError(err error) *apierrors.APIError {
|
||||
registry := c.errorRegistry
|
||||
if provider, ok := c.encoder.(response.ErrorRegistryProvider); ok {
|
||||
if scoped := provider.ErrorRegistry(); scoped != nil {
|
||||
registry = scoped
|
||||
}
|
||||
}
|
||||
return registry.Resolve(err)
|
||||
}
|
||||
|
||||
// RecordError preserves failure semantics when a middleware commits buffered bytes.
|
||||
func (c *Context) RecordError(err error) { c.failure = c.ResolveError(err) }
|
||||
|
||||
// Failure returns a detached description of a failure already handled by Context.
|
||||
func (c *Context) Failure() *apierrors.APIError {
|
||||
if c.failure == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *c.failure
|
||||
return ©
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user