Skip to content

Surface key, message, and faults in ResponseError#62

Open
pmenendz wants to merge 1 commit intomainfrom
response-error-faults
Open

Surface key, message, and faults in ResponseError#62
pmenendz wants to merge 1 commit intomainfrom
response-error-faults

Conversation

@pmenendz
Copy link
Copy Markdown
Contributor

@pmenendz pmenendz commented Apr 24, 2026

Summary

  • Rename ResponseError.CodeKey to match the API's actual error envelope (the server sends key, not code, at the top level — the old field never matched and was always empty).
  • Add ResponseError.Faults []*Fault so structured validation details from the API (e.g. GOBL-TAX-IDENTITY-01 on specific JSON paths) are captured instead of discarded.
  • Rewrite ResponseError.Error() to compose a string from whatever the server provided (status: key: message: fault1; fault2; ...), falling back to the raw response body when nothing matches.

Motivation

On an invalid silo upsert, callers were seeing errors like:

storing document in silo: updating silo entry: invalid: 422:

…with no diagnostic detail, even though the API was returning a full validation payload:

{
  "key": "validation",
  "faults": [
    {
      "code": "GOBL-TAX-IDENTITY-01",
      "paths": ["$.data.doc.customer.tax_id.country"],
      "message": "tax id country code is always required"
    }
  ],
  "fields": {...}
}

The Code field on ResponseError had the JSON tag code, but the API envelope (api/internal/interfaces/web/utils/errors.go) uses key. So Code was never populated, the if r.Code != "" branch in Error() never triggered, and the output collapsed to "422: ".

With this change, the same error now surfaces as:

422: validation: GOBL-TAX-IDENTITY-01: tax id country code is always required

Compatibility

  • StatusCode(), Response(), IsConflict, IsNotFound, IsForbidden, AsResponseError, and errors.As/errors.Is behavior are all unchanged — they never read the renamed field.
  • Searched every Invopop repo under the workspace — no caller references ResponseError.Code, so the rename is safe.
  • Fields *Dict is preserved; the API also auto-populates fields from faults for backward compatibility.
  • The Fault type is reused from transform_jobs.go (already a superset of the envelope Fault shape).

Test plan

  • go build ./...
  • go test ./invopop/...
  • Verify against a real 422 from a downstream service (chargebee) that the error string is now informative.

🤖 Generated with Claude Code

The API's error envelope uses `key` (not `code`) at the top level and
now includes a `faults` array for structured validation details. The
old ResponseError struct looked for `json:"code"`, which the API has
never sent, so ResponseError.Error() produced strings like "422: "
with no context.

Rename Code -> Key to match the wire format, add Faults, and rewrite
Error() to surface whatever the server provided, falling back to the
raw response body when no known fields are populated.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates ResponseError handling in the invopop Go client to better surface server-provided error details (key/message/faults) and improves dependency versions in the module.

Changes:

  • Renames ResponseError.Code to ResponseError.Key and updates JSON tags to match the API error envelope.
  • Captures structured validation details via ResponseError.Faults and composes richer Error() strings (including a raw-body fallback).
  • Bumps several Go module dependencies in go.mod/go.sum.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.

File Description
invopop/errors.go Enhances response error decoding and string formatting; adds fault support and a fault description helper.
go.mod Updates required dependency versions.
go.sum Updates checksums to match the dependency bumps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread invopop/errors.go
Comment on lines +65 to +67
fs = append(fs, f.describe())
}
parts = append(parts, strings.Join(fs, "; "))
Comment thread invopop/errors.go
Comment on lines 54 to +74
func (r *ResponseError) Error() string {
if r.Code != "" {
return fmt.Sprintf("%d: (%s) %s", r.response.StatusCode(), r.Code, r.Message)
parts := []string{fmt.Sprintf("%d", r.response.StatusCode())}
if r.Key != "" {
parts = append(parts, r.Key)
}
if r.Message != "" {
parts = append(parts, r.Message)
}
if len(r.Faults) > 0 {
fs := make([]string, 0, len(r.Faults))
for _, f := range r.Faults {
fs = append(fs, f.describe())
}
parts = append(parts, strings.Join(fs, "; "))
}
if len(parts) == 1 {
if body := strings.TrimSpace(r.response.String()); body != "" {
parts = append(parts, body)
}
}
return strings.Join(parts, ": ")
Comment thread go.mod
Comment on lines +12 to +18
github.com/invopop/gobl v0.401.0
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.13.4
github.com/labstack/echo/v4 v4.15.0
github.com/magefile/mage v1.15.0
github.com/nats-io/nats.go v1.43.0
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants