Repository: slack-go/slack (go)

Issue:
Requesting the `InviteUsersToConversation(..)` to return user details on failed calls
### Description
As explained in the [`conversations.invite` `Usage info` documentation](https://api.slack.com/methods/conversations.invite#markdown) including a single bad user ID in an `conversations.invite` request will cause all the invites to fail.  In such cases, the API response includes an `errors` list containing user details as well as an `error` explaining the reason for each respective invite failure (as shown below).
```json
{
    "ok": false,
    "error": "user_not_found",
    "errors": [
        {
            "user": "U111111",
            "ok": false,
            "error": "user_not_found"
        },
        {
            "user": "U222222",
            "ok": false,
            "error": "cant_invite_self"
        }
    ]
}
```  
In the current state, the `slack-go` module doesn't expose any of these user details in the `error` value returned from the `InviteUsersToConversation` client method.  It just solely returns the `error` value from the API response (ex: `cant_invite`, `already_in_channel`, etc):
https://github.com/slack-go/slack/blob/abcbf2b079f6e1ab0d843639057bf5c829e9032c/conversation.go#L322

This is problematic if you wish to implement a wrapper around the client which is resilient to such failures, as there is no easy way to determine which user IDs in your request caused the failure.
 
### Potential Solution
One potential solution for this issue could be to use [`errors.Join`](https://pkg.go.dev/errors#Join) to include additional `error`s with the relevant user details for each failure.  I imagine there's some backwards compatibility concerns to consider for existing clients there, but nonetheless wanted to bring this issue to your attention.  Thanks!

Pull request:
feat: return Errors field in SlackErrorResponse
There are a few methods in the Slack API that return an `Errors` field.

We've historically not captured this. It seems there are 4:
- [`https://slack.com/api/conversations.invite`](https://api.slack.com/methods/conversations.invite)
- [`https://slack.com/api/apps.manifest.create`](https://api.slack.com/methods/apps.manifest.create)
- [`dialog submission`](https://github.com/slackapi/java-slack-sdk/blob/f8d2bceb46737285d7bda976037d27edabd4828b/slack-app-backend/src/main/java/com/slack/api/app_backend/dialogs/response/Error.java)
  This one is a bit odd, I only found it in the java docs, not sure where it is in the API docs. In any case, we don't support it in the library (_I think_)
- [`https://slack.com/api/admin.workflows.collaborators.add`](https://api.slack.com/methods/admin.workflows.collaborators.add)

We do *not* support the last two in the library therefore I didn't even add at this stage.

This PR adds support for the first two in the list.

Fixes #1433.

Patch:
diff --git a/examples/conversations_invite/conversations_invite.go b/examples/conversations_invite/conversations_invite.go
new file mode 100644
index 0000000..8b977ce
--- /dev/null
+++ b/examples/conversations_invite/conversations_invite.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"os"
+
+	"github.com/slack-go/slack"
+)
+
+func main() {
+	userToken := os.Getenv("SLACK_USER_TOKEN")
+	if userToken == "" {
+		fmt.Fprintf(os.Stderr, "SLACK_USER_TOKEN must be set.\n")
+		os.Exit(1)
+	}
+
+	api := slack.New(userToken)
+	_, err := api.InviteUsersToConversation(
+		"C1234567890", // Replace with the actual channel ID you want to invite the user to
+		"U1234567890", // Replace with the actual user ID you want to invite
+	)
+	if err != nil {
+		var errorResponse slack.SlackErrorResponse
+		if errors.As(err, &errorResponse) {
+			for _, e := range errorResponse.Errors {
+				if e.ConversationsInviteResponseError != nil {
+					fmt.Fprintf(os.Stderr, "error inviting user (%s) to conversation: %s\n", e.ConversationsInviteResponseError.User, e.ConversationsInviteResponseError.Error)
+				}
+			}
+		} else {
+			fmt.Fprintf(os.Stderr, "error inviting user to conversation: %s\n", err.Error())
+		}
+		os.Exit(1)
+	}
+	fmt.Println("User invited successfully to the conversation.")
+}
diff --git a/misc.go b/misc.go
index f06108e..7a0ab35 100644
--- a/misc.go
+++ b/misc.go
@@ -19,11 +19,90 @@ import (
 	"time"
 )
 
+// Apps Manifest Create Response Errors ("/apps.manifest.create")
+type AppsManifestCreateResponseError struct {
+	Code             string `json:"code,omitempty"`
+	Message          string `json:"message"`
+	Pointer          string `json:"pointer"`
+	RelatedComponent string `json:"related_component,omitempty"`
+}
+
+// Conversations Invite Response Errors ("/conversations.invite")
+type ConversationsInviteResponseError struct {
+	Error string `json:"error"`
+	Ok    bool   `json:"ok"`
+	User  string `json:"user"`
+}
+
+func (t ConversationsInviteResponseError) Err() error {
+	if !t.Ok {
+		return fmt.Errorf("conversations invite error (user: %s): %s", t.User, t.Error)
+	}
+	return nil
+}
+
+// SlackResponseErrors represents a union type for different error structures
+type SlackResponseErrors struct {
+	AppsManifestCreateResponseError  *AppsManifestCreateResponseError  `json:"-"`
+	ConversationsInviteResponseError *ConversationsInviteResponseError `json:"-"`
+}
+
+// MarshalJSON implements custom marshaling for SlackResponseErrors
+func (e SlackResponseErrors) MarshalJSON() ([]byte, error) {
+	if e.AppsManifestCreateResponseError != nil {
+		return json.Marshal(e.AppsManifestCreateResponseError)
+	}
+	if e.ConversationsInviteResponseError != nil {
+		return json.Marshal(e.ConversationsInviteResponseError)
+	}
+	return json.Marshal(nil)
+}
+
+// UnmarshalJSON implements custom unmarshaling for SlackResponseErrors
+func (e *SlackResponseErrors) UnmarshalJSON(data []byte) error {
+	if bytes.Equal(data, []byte("null")) {
+		return nil
+	}
+
+	// Try to determine the error type by checking for unique fields
+	var raw map[string]interface{}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+
+	if _, hasPointer := raw["pointer"]; hasPointer {
+		if _, hasMessage := raw["message"]; hasMessage {
+			var amc AppsManifestCreateResponseError
+			if err := json.Unmarshal(data, &amc); err != nil {
+				return err
+			}
+			e.AppsManifestCreateResponseError = &amc
+			return nil
+		}
+	}
+
+	if _, hasUser := raw["user"]; hasUser {
+		if _, hasError := raw["error"]; hasError {
+			if _, hasOk := raw["ok"]; hasOk {
+				var ci ConversationsInviteResponseError
+				if err := json.Unmarshal(data, &ci); err != nil {
+					return err
+				}
+				e.ConversationsInviteResponseError = &ci
+				return nil
+			}
+		}
+	}
+
+	return fmt.Errorf("unknown error structure: %s", string(data))
+}
+
 // SlackResponse handles parsing out errors from the web api.
 type SlackResponse struct {
-	Ok               bool             `json:"ok"`
-	Error            string           `json:"error"`
-	ResponseMetadata ResponseMetadata `json:"response_metadata"`
+	Ok               bool                  `json:"ok"`
+	Error            string                `json:"error"`
+	Errors           []SlackResponseErrors `json:"errors,omitempty"`
+	ResponseMetadata ResponseMetadata      `json:"response_metadata"`
 }
 
 func (t SlackResponse) Err() error {
@@ -38,12 +117,13 @@ func (t SlackResponse) Err() error {
 		return nil
 	}
 
-	return SlackErrorResponse{Err: t.Error, ResponseMetadata: t.ResponseMetadata}
+	return SlackErrorResponse{Err: t.Error, Errors: t.Errors, ResponseMetadata: t.ResponseMetadata}
 }
 
 // SlackErrorResponse brings along the metadata of errors returned by the Slack API.
 type SlackErrorResponse struct {
 	Err              string
+	Errors           []SlackResponseErrors
 	ResponseMetadata ResponseMetadata
 }
 


Test patch:
diff --git a/misc_test.go b/misc_test.go
index 637cb77..06fab1e 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -2,6 +2,7 @@ package slack
 
 import (
 	"context"
+	"encoding/json"
 	"log"
 	"net/http"
 	"net/url"
@@ -104,3 +105,187 @@ func TestRetryable(t *testing.T) {
 		}
 	}
 }
+
+func TestSlackResponseErrorsMarshaling(t *testing.T) {
+	tests := []struct {
+		name     string
+		errors   SlackResponseErrors
+		expected string
+	}{
+		{
+			name: "AppsManifestCreateResponseError",
+			errors: SlackResponseErrors{
+				AppsManifestCreateResponseError: &AppsManifestCreateResponseError{
+					Message: "Interactivity requires Socket Mode enabled",
+					Pointer: "/settings/interactivity",
+				},
+			},
+			expected: `{"message":"Interactivity requires Socket Mode enabled","pointer":"/settings/interactivity"}`,
+		},
+		{
+			name: "ConversationsInviteResponseError",
+			errors: SlackResponseErrors{
+				ConversationsInviteResponseError: &ConversationsInviteResponseError{
+					Error: "invalid_user",
+					Ok:    false,
+					User:  "U12345678",
+				},
+			},
+			expected: `{"error":"invalid_user","ok":false,"user":"U12345678"}`,
+		},
+		{
+			name:     "EmptyErrors",
+			errors:   SlackResponseErrors{},
+			expected: `null`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			data, err := json.Marshal(tt.errors)
+			if err != nil {
+				t.Fatalf("Marshal failed: %v", err)
+			}
+			if string(data) != tt.expected {
+				t.Errorf("got %s; want %s", string(data), tt.expected)
+			}
+		})
+	}
+}
+
+func TestSlackResponseErrorsUnmarshaling(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected SlackResponseErrors
+	}{
+		{
+			name:  "AppsManifestCreateResponseError",
+			input: `{"pointer":"/settings/interactivity","message":"Interactivity requires Socket Mode enabled"}`,
+			expected: SlackResponseErrors{
+				AppsManifestCreateResponseError: &AppsManifestCreateResponseError{
+					Pointer: "/settings/interactivity",
+					Message: "Interactivity requires Socket Mode enabled",
+				},
+			},
+		},
+		{
+			name:  "ConversationsInviteResponseError",
+			input: `{"error":"invalid_user","ok":false,"user":"U12345678"}`,
+			expected: SlackResponseErrors{
+				ConversationsInviteResponseError: &ConversationsInviteResponseError{
+					Error: "invalid_user",
+					Ok:    false,
+					User:  "U12345678",
+				},
+			},
+		},
+		{
+			name:     "NullInput",
+			input:    `null`,
+			expected: SlackResponseErrors{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var errors SlackResponseErrors
+			err := json.Unmarshal([]byte(tt.input), &errors)
+			if err != nil {
+				t.Fatalf("Unmarshal failed: %v", err)
+			}
+
+			if tt.expected.AppsManifestCreateResponseError != nil {
+				if errors.AppsManifestCreateResponseError == nil {
+					t.Error("expected AppsManifestCreateResponseError, got nil")
+				} else if *errors.AppsManifestCreateResponseError != *tt.expected.AppsManifestCreateResponseError {
+					t.Errorf("got %+v; want %+v", *errors.AppsManifestCreateResponseError, *tt.expected.AppsManifestCreateResponseError)
+				}
+			}
+
+			if tt.expected.ConversationsInviteResponseError != nil {
+				if errors.ConversationsInviteResponseError == nil {
+					t.Error("expected ConversationsInviteResponseError, got nil")
+				} else if *errors.ConversationsInviteResponseError != *tt.expected.ConversationsInviteResponseError {
+					t.Errorf("got %+v; want %+v", *errors.ConversationsInviteResponseError, *tt.expected.ConversationsInviteResponseError)
+				}
+			}
+		})
+	}
+}
+
+func TestSlackResponseErrorsUnmarshalingUnknownStructure(t *testing.T) {
+	input := `{"unknown_field":"value","other_field":123}`
+	var errors SlackResponseErrors
+	err := json.Unmarshal([]byte(input), &errors)
+	if err == nil {
+		t.Error("expected error for unknown structure, got nil")
+	}
+	expectedError := "unknown error structure: " + input
+	if err.Error() != expectedError {
+		t.Errorf("got error %q; want %q", err.Error(), expectedError)
+	}
+}
+
+func TestSlackResponseWithErrors(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected SlackResponse
+	}{
+		{
+			name:  "ResponseWithAppsManifestCreateResponseError",
+			input: `{"ok":false,"error":"invalid_manifest","errors":[{"pointer":"/settings/interactivity","message":"Interactivity requires Socket Mode enabled"}]}`,
+			expected: SlackResponse{
+				Ok:    false,
+				Error: "invalid_manifest",
+				Errors: []SlackResponseErrors{
+					{
+						AppsManifestCreateResponseError: &AppsManifestCreateResponseError{
+							Pointer: "/settings/interactivity",
+							Message: "Interactivity requires Socket Mode enabled",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:  "ResponseWithoutErrors",
+			input: `{"ok":true}`,
+			expected: SlackResponse{
+				Ok: true,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var response SlackResponse
+			err := json.Unmarshal([]byte(tt.input), &response)
+			if err != nil {
+				t.Fatalf("Unmarshal failed: %v", err)
+			}
+
+			if response.Ok != tt.expected.Ok {
+				t.Errorf("got Ok=%v; want Ok=%v", response.Ok, tt.expected.Ok)
+			}
+			if response.Error != tt.expected.Error {
+				t.Errorf("got Error=%q; want Error=%q", response.Error, tt.expected.Error)
+			}
+
+			if tt.expected.Errors == nil {
+				if response.Errors != nil {
+					t.Error("expected nil Errors, got non-nil")
+				}
+			} else {
+				if response.Errors == nil {
+					t.Error("expected non-nil Errors, got nil")
+					return
+				}
+				if len(tt.expected.Errors) == 0 {
+					t.Errorf("got Errors=%v; want Errors=%v", response.Errors, tt.expected.Errors)
+				}
+			}
+		})
+	}
+}