Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions internal/clierr/clierr.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"syscall"

"github.com/a2aproject/a2a-go/v2/a2a"
"github.com/a2aproject/a2a-go/v2/a2aclient/agentcard"
)

// Code is a symbolic error identifier.
Expand Down Expand Up @@ -111,11 +114,26 @@ func CardResolution(err error) *Error {
Code: CodeCardInvalid,
Message: msg,
Exit: 4,
Hint: "check the --agent-card reference (host, full card URL, or local file path)",
Hint: cardInvalidHint(err),
err: err,
}
}

// cardInvalidHint picks remediation for a card that was reached but not usable,
// based on the HTTP status of the card request when there is one.
func cardInvalidHint(err error) string {
var status *agentcard.ErrStatusNotOK
if errors.As(err, &status) {
switch {
case status.StatusCode == http.StatusUnauthorized || status.StatusCode == http.StatusForbidden:
return "pass credentials with --auth, --svc-param or --credential-helper"
case status.StatusCode >= http.StatusInternalServerError:
return "the agent failed to serve its card, check its logs or retry"
}
}
return "check the --agent-card reference (host, full card URL, or local file path)"
}

// MarshalJSON implements [json.Marshaler].
func (e *Error) MarshalJSON() ([]byte, error) {
type body struct {
Expand Down Expand Up @@ -186,10 +204,12 @@ func Classify(err error) *Error {

// isIOError reports whether err is a transport-level failure — a network error,
// a broken stream, or a reset/closed connection — identified by type and
// sentinel rather than by matching message text.
// sentinel rather than by matching message text. A local file error is not one,
// even though the syscall.Errno it wraps satisfies net.Error.
func isIOError(err error) bool {
var netErr net.Error
if errors.As(err, &netErr) {
var pathErr *fs.PathError
if errors.As(err, &netErr) && !errors.As(err, &pathErr) {
return true
}
return errors.Is(err, io.ErrUnexpectedEOF) ||
Expand Down
35 changes: 35 additions & 0 deletions internal/clierr/clierr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"net"
"syscall"
"testing"

"github.com/a2aproject/a2a-go/v2/a2a"
"github.com/a2aproject/a2a-go/v2/a2aclient/agentcard"
)

func TestClassify(t *testing.T) {
Expand Down Expand Up @@ -85,6 +87,18 @@ func TestClassify(t *testing.T) {
wantCode: CodeIO,
wantExit: 3,
},
{
name: "local file error is not io, exit 1",
err: fmt.Errorf("failed to save file parts: %w", &fs.PathError{Op: "open", Path: "out/x.txt", Err: syscall.EACCES}),
wantCode: CodeInternal,
wantExit: 1,
},
{
name: "broken pipe on a file is still io",
err: fmt.Errorf("writing to plugin: %w", &fs.PathError{Op: "write", Path: "|1", Err: syscall.EPIPE}),
wantCode: CodeIO,
wantExit: 3,
},
{
name: "unclassified error is internal, exit 1",
err: errors.New("something odd happened"),
Expand Down Expand Up @@ -158,6 +172,27 @@ func TestCardResolution(t *testing.T) {
wantExit: 4,
wantHint: "check the --agent-card reference (host, full card URL, or local file path)",
},
{
name: "unauthorized card request asks for credentials",
err: &agentcard.ErrStatusNotOK{StatusCode: 401, Status: "401 Unauthorized"},
wantCode: CodeCardInvalid,
wantExit: 4,
wantHint: "pass credentials with --auth, --svc-param or --credential-helper",
},
{
name: "forbidden card request asks for credentials",
err: &agentcard.ErrStatusNotOK{StatusCode: 403, Status: "403 Forbidden"},
wantCode: CodeCardInvalid,
wantExit: 4,
wantHint: "pass credentials with --auth, --svc-param or --credential-helper",
},
{
name: "server error on card request points at the agent",
err: &agentcard.ErrStatusNotOK{StatusCode: 500, Status: "500 Internal Server Error"},
wantCode: CodeCardInvalid,
wantExit: 4,
wantHint: "the agent failed to serve its card, check its logs or retry",
},
{
name: "deadline exceeded is a timeout",
err: context.DeadlineExceeded,
Expand Down
Loading