44package base
55
66import (
7+ "errors"
8+ "fmt"
79 "strings"
810
9- "github.com/larksuite/cli/internal/output"
11+ "github.com/larksuite/cli/errs"
12+ "github.com/larksuite/cli/extension/fileio"
13+ "github.com/larksuite/cli/internal/errclass"
1014 "github.com/larksuite/cli/internal/util"
1115)
1216
@@ -24,74 +28,196 @@ func handleBaseAPIResult(result interface{}, err error, action string) (map[stri
2428// structured ErrAPI, with server-provided message/hint promoted to the top level.
2529func handleBaseAPIResultAny (result interface {}, err error , action string ) (interface {}, error ) {
2630 if err != nil {
27- return nil , output . Errorf ( output . ExitAPI , "api_error" , "%s: %s" , action , err )
31+ return nil , baseAPIBoundaryError ( err , action )
2832 }
2933
30- resultMap , _ := result .(map [string ]interface {})
31- code , _ := util .ToFloat64 (resultMap ["code" ])
34+ resultMap , ok := result .(map [string ]interface {})
35+ if ! ok || resultMap == nil {
36+ return nil , errs .NewInternalError (errs .SubtypeInvalidResponse , "%s: API returned a malformed response envelope" , action )
37+ }
38+ if _ , exists := resultMap ["code" ]; ! exists {
39+ return nil , errs .NewInternalError (errs .SubtypeInvalidResponse , "%s: API response is missing code" , action )
40+ }
41+ code , numeric := util .ToFloat64 (resultMap ["code" ])
42+ if ! numeric {
43+ return nil , errs .NewInternalError (errs .SubtypeInvalidResponse , "%s: API response code is not numeric" , action )
44+ }
3245 if code == 0 {
3346 return resultMap ["data" ], nil
3447 }
3548
36- larkCode := int (code )
37- msg := extractDataErrorMessage (resultMap )
38- if strings .TrimSpace (msg ) == "" {
39- msg , _ = resultMap ["msg" ].(string )
49+ return nil , baseAPIErrorFromResult (resultMap , errclass.ClassifyContext {})
50+ }
51+
52+ // baseFlagErrorf marks flag-usage failures; it shares baseValidationErrorf's
53+ // typed envelope and exists so call sites read as flag rejections.
54+ func baseFlagErrorf (format string , args ... any ) error {
55+ return baseValidationErrorf (format , args ... )
56+ }
57+
58+ func baseValidationErrorf (format string , args ... any ) error {
59+ msg := fmt .Sprintf (format , args ... )
60+ err := errs .NewValidationError (errs .SubtypeInvalidArgument , "%s" , msg )
61+ if params := flagParams (msg ); len (params ) > 0 {
62+ err = err .WithParam (params [0 ].Name ).WithParams (params ... )
63+ }
64+ if cause := firstErrorArg (args ); cause != nil {
65+ err = err .WithCause (cause )
4066 }
67+ return err
68+ }
4169
42- detail := extractErrorDetail (resultMap )
43- apiErr := output .ErrAPI (larkCode , msg , detail )
44- hint := extractErrorHint (resultMap )
45- if apiErr .Detail != nil && apiErr .Detail .Hint == "" && hint != "" {
46- apiErr .Detail .Hint = hint
70+ func flagParams (msg string ) []errs.InvalidParam {
71+ reason := msg
72+ seen := map [string ]bool {}
73+ params := []errs.InvalidParam {}
74+ for start := strings .Index (msg , "--" ); start >= 0 ; start = strings .Index (msg , "--" ) {
75+ end := start + 2
76+ for end < len (msg ) {
77+ ch := msg [end ]
78+ if (ch >= 'a' && ch <= 'z' ) || (ch >= 'A' && ch <= 'Z' ) || (ch >= '0' && ch <= '9' ) || ch == '-' {
79+ end ++
80+ continue
81+ }
82+ break
83+ }
84+ if end > start + 2 {
85+ name := msg [start :end ]
86+ if ! seen [name ] {
87+ seen [name ] = true
88+ params = append (params , errs.InvalidParam {Name : name , Reason : reason })
89+ }
90+ }
91+ msg = msg [end :]
4792 }
48- if apiErr .Detail != nil {
49- apiErr .Detail .Detail = cleanEmptyBaseErrorDetail (detail )
93+ return params
94+ }
95+
96+ func firstErrorArg (args []any ) error {
97+ for _ , arg := range args {
98+ if err , ok := arg .(error ); ok {
99+ return err
100+ }
50101 }
51- return nil , apiErr
102+ return nil
103+ }
104+
105+ // baseMissingFileIOError reports a broken runtime wiring: a command that needs
106+ // local file access was constructed without a FileIO provider. The user cannot
107+ // fix this by changing flags, so it classifies as internal, not validation.
108+ func baseMissingFileIOError (format string , args ... any ) error {
109+ return errs .NewInternalError (errs .SubtypeFileIO , format , args ... )
52110}
53111
54- func cleanEmptyBaseErrorDetail (detail interface {}) interface {} {
55- detailMap , ok := detail .(map [string ]interface {})
56- if ! ok {
112+ func baseInputStatError (err error ) error {
113+ if err == nil {
57114 return nil
58115 }
59- for key , value := range detailMap {
60- if value == nil {
61- delete (detailMap , key )
62- }
116+ if errors .Is (err , fileio .ErrPathValidation ) {
117+ return errs .NewValidationError (errs .SubtypeInvalidArgument , "unsafe file path: %s" , err ).WithCause (err )
63118 }
64- if len (detailMap ) == 0 {
119+ return errs .NewValidationError (errs .SubtypeInvalidArgument , "cannot read file: %s" , err ).WithCause (err )
120+ }
121+
122+ func baseSaveError (err error ) error {
123+ if err == nil {
65124 return nil
66125 }
67- return detailMap
126+ var me * fileio.MkdirError
127+ switch {
128+ case errors .Is (err , fileio .ErrPathValidation ):
129+ return errs .NewValidationError (errs .SubtypeInvalidArgument , "unsafe output path: %s" , err ).WithCause (err )
130+ case errors .As (err , & me ):
131+ return errs .NewInternalError (errs .SubtypeFileIO , "cannot create parent directory: %s" , err ).WithCause (err )
132+ default :
133+ return errs .NewInternalError (errs .SubtypeFileIO , "cannot create file: %s" , err ).WithCause (err )
134+ }
68135}
69136
70- func extractErrorDetail ( resultMap map [ string ] interface {}) interface {} {
71- if detail , ok := nonNilMapValue ( resultMap , "error" ); ok {
72- return detail
137+ func baseAPIBoundaryError ( err error , action string ) error {
138+ if _ , ok := errs . ProblemOf ( err ); ok {
139+ return err
73140 }
74- data , _ := resultMap ["data" ].(map [string ]interface {})
75- if detail , ok := nonNilMapValue (data , "error" ); ok {
76- return detail
141+ return errs .NewNetworkError (errs .SubtypeNetworkTransport , "%s: %s" , action , err ).WithCause (err )
142+ }
143+
144+ func baseUploadAttachmentError (filePath string , err error ) error {
145+ if p , ok := errs .ProblemOf (err ); ok {
146+ p .Message = fmt .Sprintf ("failed to upload attachment %s: %s" , filePath , p .Message )
147+ return err
77148 }
78- return nil
149+ return errs . NewInternalError ( errs . SubtypeSDKError , "failed to upload attachment %s: %s" , filePath , err ). WithCause ( err )
79150}
80151
81- func nonNilMapValue ( src map [string ]interface {}, key string ) ( interface {}, bool ) {
82- if src == nil {
83- return nil , false
152+ func baseAPIErrorFromResult ( resultMap map [string ]interface {}, cc errclass. ClassifyContext ) error {
153+ if resultMap == nil {
154+ return errs . NewInternalError ( errs . SubtypeInvalidResponse , "API returned a malformed response envelope" )
84155 }
85- value , ok := src [key ]
86- if ! ok {
87- return nil , false
156+ if msg := extractDataErrorMessage (resultMap ); msg != "" {
157+ resultMap ["msg" ] = msg
88158 }
89- switch value .(type ) {
90- case nil :
91- return nil , false
92- default :
93- return value , true
159+ hint := extractErrorHint (resultMap )
160+ if logID := extractBaseErrorLogID (resultMap ); logID != "" {
161+ resultMap ["log_id" ] = logID
162+ }
163+ err := errclass .BuildAPIError (resultMap , cc )
164+ if err == nil {
165+ return nil
166+ }
167+ if p , ok := errs .ProblemOf (err ); ok && hint != "" {
168+ p .Hint = hint
169+ }
170+ return err
171+ }
172+
173+ func enrichBaseAPIErrorFromBody (err error , body []byte , cc errclass.ClassifyContext ) error {
174+ if _ , ok := errs .ProblemOf (err ); ! ok {
175+ return err
176+ }
177+ result , parseErr := decodeBaseV3Response (body )
178+ if parseErr != nil {
179+ return err
180+ }
181+ enriched := baseAPIErrorFromResult (result , cc )
182+ if enriched == nil {
183+ return err
184+ }
185+ src , _ := errs .ProblemOf (enriched )
186+ dst , _ := errs .ProblemOf (err )
187+ if src != nil && dst != nil {
188+ dst .Message = src .Message
189+ dst .Hint = src .Hint
190+ // A body without log_id must not erase a header-derived LogID
191+ // already carried by err.
192+ if src .LogID != "" {
193+ dst .LogID = src .LogID
194+ }
94195 }
196+ return err
197+ }
198+
199+ func extractBaseErrorLogID (resultMap map [string ]interface {}) string {
200+ for _ , key := range []string {"log_id" , "logid" } {
201+ if logID , _ := resultMap [key ].(string ); strings .TrimSpace (logID ) != "" {
202+ return strings .TrimSpace (logID )
203+ }
204+ }
205+ if detail , ok := resultMap ["error" ].(map [string ]interface {}); ok {
206+ for _ , key := range []string {"log_id" , "logid" } {
207+ if logID , _ := detail [key ].(string ); strings .TrimSpace (logID ) != "" {
208+ return strings .TrimSpace (logID )
209+ }
210+ }
211+ }
212+ data , _ := resultMap ["data" ].(map [string ]interface {})
213+ if detail , ok := data ["error" ].(map [string ]interface {}); ok {
214+ for _ , key := range []string {"log_id" , "logid" } {
215+ if logID , _ := detail [key ].(string ); strings .TrimSpace (logID ) != "" {
216+ return strings .TrimSpace (logID )
217+ }
218+ }
219+ }
220+ return ""
95221}
96222
97223func extractErrorHint (resultMap map [string ]interface {}) string {
0 commit comments