feat(api): return structured JSON error responses

The API error handling is updated to return a structured JSON payload
instead of a plain text message. This provides more context and allows
clients to programmatically handle different error types.

The new error response has the following structure:
{
  "error": {
    "message": "...",
    "type": "..."
  }
}

The `type` field is determined by the HTTP status code, such as
`authentication_error`, `rate_limit_error`, or `server_error`.

If the underlying error message from an upstream service is already a
valid JSON string, it will be preserved and returned directly.

BREAKING CHANGE: API error responses are now in a structured JSON
format instead of plain text. Clients expecting plain text error
messages will need to be updated to parse the new JSON body.
This commit is contained in:
hkfires
2025-12-15 16:18:44 +08:00
parent f6720f8dfa
commit 4c07ea41c3

View File

@@ -5,6 +5,7 @@ package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -437,12 +438,50 @@ func (h *BaseAPIHandler) WriteErrorResponse(c *gin.Context, msg *interfaces.Erro
}
}
}
c.Status(status)
errText := http.StatusText(status)
if msg != nil && msg.Error != nil {
_, _ = c.Writer.Write([]byte(msg.Error.Error()))
} else {
_, _ = c.Writer.Write([]byte(http.StatusText(status)))
if v := strings.TrimSpace(msg.Error.Error()); v != "" {
errText = v
}
}
// Prefer preserving upstream JSON error bodies when possible.
buildJSONBody := func() []byte {
trimmed := strings.TrimSpace(errText)
if trimmed != "" && json.Valid([]byte(trimmed)) {
return []byte(trimmed)
}
errType := "invalid_request_error"
switch status {
case http.StatusUnauthorized:
errType = "authentication_error"
case http.StatusForbidden:
errType = "permission_error"
case http.StatusTooManyRequests:
errType = "rate_limit_error"
default:
if status >= http.StatusInternalServerError {
errType = "server_error"
}
}
payload, err := json.Marshal(ErrorResponse{
Error: ErrorDetail{
Message: errText,
Type: errType,
},
})
if err != nil {
return []byte(fmt.Sprintf(`{"error":{"message":%q,"type":"server_error"}}`, errText))
}
return payload
}
if !c.Writer.Written() {
c.Writer.Header().Set("Content-Type", "application/json")
}
c.Status(status)
_, _ = c.Writer.Write(buildJSONBody())
}
func (h *BaseAPIHandler) LoggingAPIResponseError(ctx context.Context, err *interfaces.ErrorMessage) {