Files
LocalAI/core/http/endpoints/studio/errmap.go
T
Ettore Di Giacinto c70566d4f7 feat(connectors): keep two connections of one bot from answering twice
Two enabled connections can hold different token pairs of the same
upstream account, such as one Slack app registered twice. Both then
receive every message and both answer it.

The op "me" may now return accountId, a stable name of the upstream
account. Slack returns the workspace and bot user, Telegram the bot ID
and Discord the application ID. The host stores it on the binding when
a token is saved and, for a binding that has none, when its add-on runs.
An add-on that does not return it has no identity and is never compared.

When two enabled bindings of one owner and kind share an identity:

- the binding carries a duplicate marker with the code
  duplicate_connection, with a sentence in all eight languages of the
  connection texts;
- saving one of them is a 409 of that type until the request carries
  confirm_duplicate, and nothing is saved before;
- only the oldest enabled binding answers a message that has a platform
  message ID. The other logs one line and stays silent. The choice reads
  the stored bindings only, so every frontend makes the same one.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-05 11:36:23 +00:00

115 lines
3.6 KiB
Go

package studio
import (
"errors"
"net/http"
"strings"
"github.com/labstack/echo/v4"
)
// msgNotJSON is the answer to a body that does not parse.
const msgNotJSON = "the body is not valid JSON"
// Error types of the chats API. The UI and clients match on them.
const (
typeInvalid = "invalid_request_error"
typeNotFound = "not_found"
typeConflict = "conflict"
typeServerError = "server_error"
typeUpstream = "upstream_error"
typeAuthz = "authorization_error"
typeFilesOff = "files_off"
typeAgentsOff = "agents_off"
typeNotifyOff = "notifications_off"
typeTriggersOff = "triggers_off"
typeConnectorOff = "connectors_off"
// typeDuplicateConnection: the connection to save is the same bot as
// another enabled one; send confirm_duplicate to save it anyway.
typeDuplicateConnection = "duplicate_connection"
// typePageNotAllowed: a shared page called its bridge without your consent
// to the version it runs (or after you revoked it).
typePageNotAllowed = "page_not_allowed"
// typeSharedPagesOff: an administrator switched the shared pages off.
typeSharedPagesOff = "shared_pages_off"
)
// errRule is one row of an error table: the errors that match (errors.Is), the
// status and type of the answer, and its text. The text is msg when it is set;
// else the text of the error, less the prefix trim, or less the sentinel strip
// and a following ": " when strip is set.
type errRule struct {
is []error
status int
typ string
msg string
trim string
strip error
}
// fixed answers with msg whatever the text of the error is, so that the answer
// never holds a detail of the failure.
func fixed(status int, typ, msg string, is ...error) errRule {
return errRule{is: is, status: status, typ: typ, msg: msg}
}
// verbatim answers with the text of the error, less the prefix trim.
func verbatim(status int, typ, trim string, is ...error) errRule {
return errRule{is: is, status: status, typ: typ, trim: trim}
}
// sentinelText answers with the text of the error after its sentinel. The
// text is written for the person who reads it.
func sentinelText(status int, typ string, sentinel error) errRule {
return errRule{is: []error{sentinel}, status: status, typ: typ, strip: sentinel}
}
func (r errRule) text(err error) string {
switch {
case r.msg != "":
return r.msg
case r.strip != nil:
return strings.TrimPrefix(strings.TrimPrefix(err.Error(), r.strip.Error()), ": ")
}
return strings.TrimPrefix(err.Error(), r.trim)
}
func (r errRule) matches(err error) bool {
for _, target := range r.is {
if errors.Is(err, target) {
return true
}
}
return false
}
// mapErr answers with the first rule that matches err. When none does it calls
// next, the mapper of the layer below.
func mapErr(c echo.Context, err error, rules []errRule, next func(echo.Context, error) error) error {
for _, r := range rules {
if r.matches(err) {
return fail(c, r.status, r.typ, r.text(err))
}
}
return next(c, err)
}
// bind reads the JSON body of the request into a T. When it cannot, it writes
// the answer and returns false: 413 for a body over the limit that the handler
// set with http.MaxBytesReader, else 400 with msg.
func bind[T any](c echo.Context, msg string) (T, bool) {
var req T
err := c.Bind(&req)
if err == nil {
return req, true
}
var tooBig *http.MaxBytesError
var httpErr *echo.HTTPError
if errors.As(err, &tooBig) || (errors.As(err, &httpErr) && httpErr.Code == http.StatusRequestEntityTooLarge) {
_ = fail(c, http.StatusRequestEntityTooLarge, typeInvalid, "the body is too large")
} else {
_ = fail(c, http.StatusBadRequest, typeInvalid, msg)
}
return req, false
}