@@ -1016,7 +1016,7 @@ type prettyErrorFormatter struct {
1016
1016
// format formats the error to the console. This error should be human
1017
1017
// readable.
1018
1018
func (p * prettyErrorFormatter ) format (err error ) {
1019
- output := cliHumanFormatError (err , & formatOpts {
1019
+ output , _ := cliHumanFormatError ("" , err , & formatOpts {
1020
1020
Verbose : p .verbose ,
1021
1021
})
1022
1022
// always trail with a newline
@@ -1030,41 +1030,66 @@ type formatOpts struct {
1030
1030
const indent = " "
1031
1031
1032
1032
// cliHumanFormatError formats an error for the CLI. Newlines and styling are
1033
- // included.
1034
- func cliHumanFormatError (err error , opts * formatOpts ) string {
1033
+ // included. The second return value is true if the error is special and the error
1034
+ // chain has custom formatting applied.
1035
+ //
1036
+ // If you change this code, you can use the cli "example-errors" tool to
1037
+ // verify all errors still look ok.
1038
+ //
1039
+ // go run main.go exp example-error <type>
1040
+ // go run main.go exp example-error api
1041
+ // go run main.go exp example-error cmd
1042
+ // go run main.go exp example-error multi-error
1043
+ // go run main.go exp example-error validation
1044
+ //
1045
+ //nolint:errorlint
1046
+ func cliHumanFormatError (from string , err error , opts * formatOpts ) (string , bool ) {
1035
1047
if opts == nil {
1036
1048
opts = & formatOpts {}
1037
1049
}
1050
+ if err == nil {
1051
+ return "<nil>" , true
1052
+ }
1038
1053
1039
- //nolint:errorlint
1040
1054
if multi , ok := err .(interface { Unwrap () []error }); ok {
1041
1055
multiErrors := multi .Unwrap ()
1042
1056
if len (multiErrors ) == 1 {
1043
1057
// Format as a single error
1044
- return cliHumanFormatError (multiErrors [0 ], opts )
1058
+ return cliHumanFormatError (from , multiErrors [0 ], opts )
1045
1059
}
1046
- return formatMultiError (multiErrors , opts )
1060
+ return formatMultiError (from , multiErrors , opts ), true
1047
1061
}
1048
1062
1049
1063
// First check for sentinel errors that we want to handle specially.
1050
1064
// Order does matter! We want to check for the most specific errors first.
1051
- var sdkError * codersdk.Error
1052
- if errors .As (err , & sdkError ) {
1053
- return formatCoderSDKError (sdkError , opts )
1065
+ if sdkError , ok := err .(* codersdk.Error ); ok {
1066
+ return formatCoderSDKError (from , sdkError , opts ), true
1054
1067
}
1055
1068
1056
- var cmdErr * clibase.RunCommandError
1057
- if errors .As (err , & cmdErr ) {
1058
- return formatRunCommandError (cmdErr , opts )
1069
+ if cmdErr , ok := err .(* clibase.RunCommandError ); ok {
1070
+ // no need to pass the "from" context to this since it is always
1071
+ // top level. We care about what is below this.
1072
+ return formatRunCommandError (cmdErr , opts ), true
1059
1073
}
1060
1074
1075
+ uw , ok := err .(interface { Unwrap () error })
1076
+ if ok {
1077
+ msg , special := cliHumanFormatError (from + traceError (err ), uw .Unwrap (), opts )
1078
+ if special {
1079
+ return msg , special
1080
+ }
1081
+ }
1082
+ // If we got here, that means that the wrapped error chain does not have
1083
+ // any special formatting below it. So we want to return the topmost non-special
1084
+ // error (which is 'err')
1085
+
1061
1086
// Default just printing the error. Use +v for verbose to handle stack
1062
1087
// traces of xerrors.
1063
1088
if opts .Verbose {
1064
- return pretty .Sprint (headLineStyle (), fmt .Sprintf ("%+v" , err ))
1089
+ return pretty .Sprint (headLineStyle (), fmt .Sprintf ("%+v" , err )), false
1065
1090
}
1066
1091
1067
- return pretty .Sprint (headLineStyle (), fmt .Sprintf ("%v" , err ))
1092
+ return pretty .Sprint (headLineStyle (), fmt .Sprintf ("%v" , err )), false
1068
1093
}
1069
1094
1070
1095
// formatMultiError formats a multi-error. It formats it as a list of errors.
@@ -1075,15 +1100,20 @@ func cliHumanFormatError(err error, opts *formatOpts) string {
1075
1100
// <verbose error message>
1076
1101
// 2. <heading error message>
1077
1102
// <verbose error message>
1078
- func formatMultiError (multi []error , opts * formatOpts ) string {
1103
+ func formatMultiError (from string , multi []error , opts * formatOpts ) string {
1079
1104
var errorStrings []string
1080
1105
for _ , err := range multi {
1081
- errorStrings = append (errorStrings , cliHumanFormatError (err , opts ))
1106
+ msg , _ := cliHumanFormatError ("" , err , opts )
1107
+ errorStrings = append (errorStrings , msg )
1082
1108
}
1083
1109
1084
1110
// Write errors out
1085
1111
var str strings.Builder
1086
- _ , _ = str .WriteString (pretty .Sprint (headLineStyle (), fmt .Sprintf ("%d errors encountered:" , len (multi ))))
1112
+ var traceMsg string
1113
+ if from != "" {
1114
+ traceMsg = fmt .Sprintf ("Trace=[%s])" , from )
1115
+ }
1116
+ _ , _ = str .WriteString (pretty .Sprint (headLineStyle (), fmt .Sprintf ("%d errors encountered: %s" , len (multi ), traceMsg )))
1087
1117
for i , errStr := range errorStrings {
1088
1118
// Indent each error
1089
1119
errStr = strings .ReplaceAll (errStr , "\n " , "\n " + indent )
@@ -1112,24 +1142,30 @@ func formatRunCommandError(err *clibase.RunCommandError, opts *formatOpts) strin
1112
1142
var str strings.Builder
1113
1143
_ , _ = str .WriteString (pretty .Sprint (headLineStyle (), fmt .Sprintf ("Encountered an error running %q" , err .Cmd .FullName ())))
1114
1144
1115
- msgString := fmt .Sprintf ("%v" , err .Err )
1116
- if opts .Verbose {
1117
- // '%+v' includes stack traces
1118
- msgString = fmt .Sprintf ("%+v" , err .Err )
1119
- }
1145
+ msgString , special := cliHumanFormatError ("" , err .Err , opts )
1120
1146
_ , _ = str .WriteString ("\n " )
1121
- _ , _ = str .WriteString (pretty .Sprint (tailLineStyle (), msgString ))
1147
+ if special {
1148
+ _ , _ = str .WriteString (msgString )
1149
+ } else {
1150
+ _ , _ = str .WriteString (pretty .Sprint (tailLineStyle (), msgString ))
1151
+ }
1152
+
1122
1153
return str .String ()
1123
1154
}
1124
1155
1125
1156
// formatCoderSDKError come from API requests. In verbose mode, add the
1126
1157
// request debug information.
1127
- func formatCoderSDKError (err * codersdk.Error , opts * formatOpts ) string {
1158
+ func formatCoderSDKError (from string , err * codersdk.Error , opts * formatOpts ) string {
1128
1159
var str strings.Builder
1129
1160
if opts .Verbose {
1130
1161
_ , _ = str .WriteString (pretty .Sprint (headLineStyle (), fmt .Sprintf ("API request error to \" %s:%s\" . Status code %d" , err .Method (), err .URL (), err .StatusCode ())))
1131
1162
_ , _ = str .WriteString ("\n " )
1132
1163
}
1164
+ // Always include this trace. Users can ignore this.
1165
+ if from != "" {
1166
+ _ , _ = str .WriteString (pretty .Sprint (headLineStyle (), fmt .Sprintf ("Trace=[%s]" , from )))
1167
+ _ , _ = str .WriteString ("\n " )
1168
+ }
1133
1169
1134
1170
_ , _ = str .WriteString (pretty .Sprint (headLineStyle (), err .Message ))
1135
1171
if err .Helper != "" {
@@ -1144,6 +1180,21 @@ func formatCoderSDKError(err *codersdk.Error, opts *formatOpts) string {
1144
1180
return str .String ()
1145
1181
}
1146
1182
1183
+ // traceError is a helper function that aides developers debugging failed cli
1184
+ // commands. When we pretty print errors, we lose the context in which they came.
1185
+ // This function adds the context back. Unfortunately there is no easy way to get
1186
+ // the prefix to: "error string: %w", so we do a bit of string manipulation.
1187
+ //
1188
+ //nolint:errorlint
1189
+ func traceError (err error ) string {
1190
+ if uw , ok := err .(interface { Unwrap () error }); ok {
1191
+ a , b := err .Error (), uw .Unwrap ().Error ()
1192
+ c := strings .TrimSuffix (a , b )
1193
+ return c
1194
+ }
1195
+ return err .Error ()
1196
+ }
1197
+
1147
1198
// These styles are arbitrary.
1148
1199
func headLineStyle () pretty.Style {
1149
1200
return cliui .DefaultStyles .Error
0 commit comments