mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
fix(addons): keep the cause of a connector failure
localConnectorCall and the kinds lookup turned every error from the connector source into a bare ErrUnavailable. VerifyTokenWithMe treats that as "could not tell" and saves the binding, so a definite refusal by the add-on was indistinguishable from an outage, and the cause was lost. An add-on refusal (invalid request, or denied for this person) now keeps its type through Refused. Other errors still read as ErrUnavailable but wrap the cause, so errors.Is and logs see it. The forwarding carrier maps the owner's 400 back to ErrInvalidRequest instead of ErrUnavailable. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
2 files changed
+96
-4
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -56,7 +57,7 @@ func (g *Gateway) localConnectorKinds(ctx context.Context) ([]connectors.Kind, e
|
||||
}
|
||||
kinds, err := source.ConnectorKinds(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrUnavailable
|
||||
return nil, fmt.Errorf("%w: %w", ErrUnavailable, err)
|
||||
}
|
||||
out := make([]connectors.Kind, len(kinds))
|
||||
for i, k := range kinds {
|
||||
@@ -86,7 +87,7 @@ func (g *Gateway) localConnectorCall(ctx context.Context, s connectors.Connector
|
||||
}
|
||||
kinds, err := source.ConnectorKinds(ctx)
|
||||
if err != nil {
|
||||
return contract.ConnectorResponse{}, ErrUnavailable
|
||||
return contract.ConnectorResponse{}, fmt.Errorf("%w: %w", ErrUnavailable, err)
|
||||
}
|
||||
found := false
|
||||
for _, k := range kinds {
|
||||
@@ -111,7 +112,12 @@ func (g *Gateway) localConnectorCall(ctx context.Context, s connectors.Connector
|
||||
}
|
||||
out, err := source.Connector(ctx, s.AddonID, req)
|
||||
if err != nil {
|
||||
return contract.ConnectorResponse{}, ErrUnavailable
|
||||
// A definite refusal of the add-on keeps its type; anything else could
|
||||
// not tell, so the caller treats it as an outage, with the cause kept.
|
||||
if refused := Refused(err); !errors.Is(refused, ErrUnavailable) && errors.Is(refused, ErrForbidden) {
|
||||
return contract.ConnectorResponse{}, refused
|
||||
}
|
||||
return contract.ConnectorResponse{}, fmt.Errorf("%w: %w", ErrUnavailable, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -136,8 +142,12 @@ func (c *Carrier) connectorRequest(ctx context.Context, method, path string, bod
|
||||
return ErrUnavailable
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusForbidden:
|
||||
return ErrForbidden
|
||||
case http.StatusBadRequest:
|
||||
// The owner relays the add-on's definite refusal; see typedError.
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK || decodeConnector(resp.Body, out) != nil {
|
||||
return ErrUnavailable
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/addons"
|
||||
"github.com/mudler/LocalAI/core/services/studio/connectors"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type fakeConnectorSource struct{ kindsErr, callErr error }
|
||||
|
||||
func (s fakeConnectorSource) ConnectorKinds(context.Context) ([]addons.ConnectorKind, error) {
|
||||
if s.kindsErr != nil {
|
||||
return nil, s.kindsErr
|
||||
}
|
||||
return []addons.ConnectorKind{{AddonID: "slack", ConnectorKind: contract.ConnectorKind{ID: "slack", Delivery: contract.DeliveryPush}}}, nil
|
||||
}
|
||||
|
||||
func (s fakeConnectorSource) Connector(context.Context, string, contract.ConnectorRequest) (contract.ConnectorResponse, error) {
|
||||
return contract.ConnectorResponse{}, s.callErr
|
||||
}
|
||||
|
||||
type allowAllConnectors struct{}
|
||||
|
||||
func (allowAllConnectors) AuthorizeConnector(_ context.Context, _ connectors.ConnectorCallScope, r contract.ConnectorRequest) (contract.ConnectorRequest, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
var _ = Describe("Connector errors", func() {
|
||||
scope := connectors.ConnectorCallScope{AddonID: "slack", Kind: "slack"}
|
||||
call := func(src fakeConnectorSource) error {
|
||||
g := &Gateway{Connectors: src, ConnectorAuthority: allowAllConnectors{}}
|
||||
_, err := g.ConnectorCall(context.Background(), scope, contract.ConnectorRequest{Op: contract.ConnectorOpMe, Kind: "slack"})
|
||||
return err
|
||||
}
|
||||
|
||||
It("keeps the cause of an outage and still reports it as unavailable", func() {
|
||||
cause := errors.New("socket closed")
|
||||
err := call(fakeConnectorSource{callErr: cause})
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(err).To(MatchError(cause))
|
||||
|
||||
_, err = (&Gateway{Connectors: fakeConnectorSource{kindsErr: cause}}).ConnectorKinds(context.Background())
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(err).To(MatchError(cause))
|
||||
})
|
||||
|
||||
It("tells a definite refusal of the add-on apart from an outage", func() {
|
||||
err := call(fakeConnectorSource{callErr: fmt.Errorf("runtime: %w", contract.ErrRefused)})
|
||||
Expect(err).To(MatchError(ErrInvalidRequest))
|
||||
Expect(err).NotTo(MatchError(ErrUnavailable))
|
||||
|
||||
err = call(fakeConnectorSource{callErr: fmt.Errorf("runtime: %w", contract.ErrDenied)})
|
||||
Expect(err).To(MatchError(ErrNotPermitted))
|
||||
Expect(err).NotTo(MatchError(ErrUnavailable))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Forwarded connector answers", func() {
|
||||
forwarded := func(status int) error {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(status) }))
|
||||
defer server.Close()
|
||||
c := &Carrier{config: CarrierConfig{OwnerAddress: server.URL}, client: server.Client()}
|
||||
return c.connectorRequest(context.Background(), "GET", "/addons/connectors/kinds", nil, &[]connectors.Kind{})
|
||||
}
|
||||
|
||||
It("keeps a refusal apart from an outage", func() {
|
||||
Expect(forwarded(http.StatusBadRequest)).To(MatchError(ErrInvalidRequest))
|
||||
Expect(forwarded(http.StatusForbidden)).To(MatchError(ErrForbidden))
|
||||
err := forwarded(http.StatusServiceUnavailable)
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(err).NotTo(MatchError(ErrForbidden))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user