Repository: slack-go/slack
Pull request #1383 (2025-03-09 14:03:40)

Return a StatusCodeError when a workspace's message limit is exceeded
Currently, this library throws a `strconv.NumError` when attempting to post messages to a Slack workspace that has exceeded its message limit. It does so because Slack returns a 429 without a `Retry-After` header for those requests.

This PR improves the situation by instead returning a `StatusCodeError`. I couldn't see any obvious way to provide a more detailed error without creating a new error class. To only additional information returned from Slack for these errors is a body that reads `message_limit_exceeded`.

nlopes: @greysteil I think you'll need to change the testing endpoint as `websocket2` is already in use by another test.



Patch:
diff --git a/block_object.go b/block_object.go
index 899ac0d..a3e78d4 100644
--- a/block_object.go
+++ b/block_object.go
@@ -122,7 +122,7 @@ func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, e
 type TextBlockObject struct {
 	Type     string `json:"type"`
 	Text     string `json:"text"`
-	Emoji    *bool  `json:"emoji"`
+	Emoji    bool   `json:"emoji,omitempty"`
 	Verbatim bool   `json:"verbatim,omitempty"`
 }
 
@@ -143,7 +143,7 @@ func (s TextBlockObject) Validate() error {
 	}
 
 	// https://github.com/slack-go/slack/issues/881
-	if s.Type == "mrkdwn" && s.Emoji != nil && *s.Emoji {
+	if s.Type == "mrkdwn" && s.Emoji {
 		return errors.New("emoji cannot be true in mrkdown")
 	}
 
@@ -161,11 +161,11 @@ func (s TextBlockObject) Validate() error {
 }
 
 // NewTextBlockObject returns an instance of a new Text Block Object
-func NewTextBlockObject(elementType, text string, emoji bool, verbatim bool) *TextBlockObject {
+func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject {
 	return &TextBlockObject{
 		Type:     elementType,
 		Text:     text,
-		Emoji:    &emoji,
+		Emoji:    emoji,
 		Verbatim: verbatim,
 	}
 }
diff --git a/misc.go b/misc.go
index df779c1..b80e92b 100644
--- a/misc.go
+++ b/misc.go
@@ -306,7 +306,7 @@ func timerReset(t *time.Timer, d time.Duration) {
 }
 
 func checkStatusCode(resp *http.Response, d Debug) error {
-	if resp.StatusCode == http.StatusTooManyRequests {
+	if resp.StatusCode == http.StatusTooManyRequests && resp.Header.Get("Retry-After") != "" {
 		retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
 		if err != nil {
 			return err


Test patch:
diff --git a/attachments_test.go b/attachments_test.go
index cbb139c..4b951f1 100644
--- a/attachments_test.go
+++ b/attachments_test.go
@@ -19,7 +19,6 @@ func TestAttachment_UnmarshalMarshalJSON_WithBlocks(t *testing.T) {
         "text": {
           "type": "mrkdwn",
           "text": "Pick something:",
-          "emoji": null,
           "verbatim": true
         },
         "accessory": {
diff --git a/block_object_test.go b/block_object_test.go
index 7311034..b8f55a4 100644
--- a/block_object_test.go
+++ b/block_object_test.go
@@ -1,12 +1,10 @@
 package slack
 
 import (
-	"encoding/json"
 	"errors"
 	"strings"
 	"testing"
 
-	"github.com/go-test/deep"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -23,7 +21,7 @@ func TestNewTextBlockObject(t *testing.T) {
 
 	assert.Equal(t, textObject.Type, "plain_text")
 	assert.Equal(t, textObject.Text, "test")
-	assert.True(t, *textObject.Emoji, "Emoji property should be true")
+	assert.True(t, textObject.Emoji, "Emoji property should be true")
 	assert.False(t, textObject.Verbatim, "Verbatim should be false")
 }
 
@@ -77,11 +75,6 @@ func TestNewOptionGroupBlockElement(t *testing.T) {
 }
 
 func TestValidateTextBlockObject(t *testing.T) {
-	emojiTrue := new(bool)
-	emojiFalse := new(bool)
-	*emojiTrue = true
-	*emojiFalse = false
-
 	tests := []struct {
 		input    TextBlockObject
 		expected error
@@ -90,34 +83,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "plain_text",
 				Text:     "testText",
-				Emoji:    emojiFalse,
-				Verbatim: false,
-			},
-			expected: nil,
-		},
-		{
-			input: TextBlockObject{
-				Type:     "plain_text",
-				Text:     "testText",
-				Emoji:    emojiTrue,
-				Verbatim: false,
-			},
-			expected: nil,
-		},
-		{
-			input: TextBlockObject{
-				Type:     "plain_text",
-				Text:     "testText",
-				Emoji:    nil,
-				Verbatim: false,
-			},
-			expected: nil,
-		},
-		{
-			input: TextBlockObject{
-				Type:     "mrkdwn",
-				Text:     "testText",
-				Emoji:    emojiFalse,
+				Emoji:    false,
 				Verbatim: false,
 			},
 			expected: nil,
@@ -126,7 +92,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     "testText",
-				Emoji:    nil,
+				Emoji:    false,
 				Verbatim: false,
 			},
 			expected: nil,
@@ -135,7 +101,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "invalid",
 				Text:     "testText",
-				Emoji:    emojiFalse,
+				Emoji:    false,
 				Verbatim: false,
 			},
 			expected: errors.New("type must be either of plain_text or mrkdwn"),
@@ -144,7 +110,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     "testText",
-				Emoji:    emojiTrue,
+				Emoji:    true,
 				Verbatim: false,
 			},
 			expected: errors.New("emoji cannot be true in mrkdown"),
@@ -153,7 +119,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     "",
-				Emoji:    emojiFalse,
+				Emoji:    false,
 				Verbatim: false,
 			},
 			expected: errors.New("text must have a minimum length of 1"),
@@ -162,7 +128,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     strings.Repeat("a", 3001),
-				Emoji:    emojiFalse,
+				Emoji:    false,
 				Verbatim: false,
 			},
 			expected: errors.New("text cannot be longer than 3000 characters"),
@@ -174,83 +140,3 @@ func TestValidateTextBlockObject(t *testing.T) {
 		assert.Equal(t, err, test.expected)
 	}
 }
-
-func TestTextBlockObject_UnmarshalJSON(t *testing.T) {
-	emojiTrue := new(bool)
-	emojiFalse := new(bool)
-	*emojiTrue = true
-	*emojiFalse = false
-
-	cases := []struct {
-		raw      []byte
-		expected TextBlockObject
-		err      error
-	}{
-		{
-			[]byte(`{"type":"plain_text","text":"testText"}`),
-			TextBlockObject{
-				Type:     "plain_text",
-				Text:     "testText",
-				Emoji:    nil,
-				Verbatim: false,
-			},
-			nil,
-		},
-		{
-			[]byte(`{"type":"plain_text","text":":+1:","emoji":true}`),
-			TextBlockObject{
-				Type:     "plain_text",
-				Text:     ":+1:",
-				Emoji:    emojiTrue,
-				Verbatim: false,
-			},
-			nil,
-		},
-		{
-			[]byte(`{"type":"plain_text","text":"No emojis allowed :(","emoji":false}`),
-			TextBlockObject{
-				Type:     "plain_text",
-				Text:     "No emojis allowed :(",
-				Emoji:    emojiFalse,
-				Verbatim: false,
-			},
-			nil,
-		},
-		{
-			[]byte(`{"type":"mrkdwn","text":"testText"}`),
-			TextBlockObject{
-				Type:     "mrkdwn",
-				Text:     "testText",
-				Emoji:    nil,
-				Verbatim: false,
-			},
-			nil,
-		},
-		{
-			[]byte(`{"type":"mrkdwn","text":"No emojis allowed :(","emoji":false}`),
-			TextBlockObject{
-				Type:     "mrkdwn",
-				Text:     "No emojis allowed :(",
-				Emoji:    emojiFalse,
-				Verbatim: false,
-			},
-			nil,
-		},
-	}
-	for _, tc := range cases {
-		var actual TextBlockObject
-		err := json.Unmarshal(tc.raw, &actual)
-		if err != nil {
-			if tc.err == nil {
-				t.Errorf("unexpected error: %s", err)
-			}
-			t.Errorf("expected error is %v, but got %v", tc.err, err)
-		}
-		if tc.err != nil {
-			t.Errorf("expected to raise an error %v", tc.err)
-		}
-		if diff := deep.Equal(actual, tc.expected); diff != nil {
-			t.Errorf("actual value does not match expected one\n%s", diff)
-		}
-	}
-}
diff --git a/block_section_test.go b/block_section_test.go
index c6bd209..8f8374e 100644
--- a/block_section_test.go
+++ b/block_section_test.go
@@ -30,7 +30,7 @@ func TestNewBlockSectionContainsAddedTextBlockAndAccessory(t *testing.T) {
 	textBlockInSection := sectionBlock.Text
 	assert.Equal(t, textBlockInSection.Text, textBlockObject.Text)
 	assert.Equal(t, textBlockInSection.Type, textBlockObject.Type)
-	assert.True(t, *textBlockInSection.Emoji)
+	assert.True(t, textBlockInSection.Emoji)
 	assert.False(t, textBlockInSection.Verbatim)
 	assert.Equal(t, sectionBlock.Accessory.ImageElement, conflictImage)
 }
diff --git a/chat_test.go b/chat_test.go
index ab326f2..917ff96 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -81,7 +81,7 @@ func TestPostMessage(t *testing.T) {
 	}
 
 	blocks := []Block{NewContextBlock("context", NewTextBlockObject(PlainTextType, "hello", false, false))}
-	blockStr := `[{"type":"context","block_id":"context","elements":[{"type":"plain_text","text":"hello","emoji":false}]}]`
+	blockStr := `[{"type":"context","block_id":"context","elements":[{"type":"plain_text","text":"hello"}]}]`
 
 	tests := map[string]messageTest{
 		"OnlyBasicProperties": {
diff --git a/interactions_test.go b/interactions_test.go
index faf5333..989690e 100644
--- a/interactions_test.go
+++ b/interactions_test.go
@@ -51,15 +51,13 @@ const (
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "launch project",
-				"emoji": false
+				"text": "launch project"
 			},
 			"blocks": [{
 				"type": "section",
 				"text": {
 				  "text": "*Sally* has requested you set the deadline for the Nano launch project",
-				  "type": "mrkdwn",
-				  "emoji": false
+				  "type": "mrkdwn"
 				},
 				"accessory": {
 				  "type": "datepicker",
@@ -67,8 +65,7 @@ const (
 				  "initial_date": "1990-04-28",
 				  "placeholder": {
 					"type": "plain_text",
-					"text": "Select a date",
-				  	"emoji": false
+					"text": "Select a date"
 				  }
 				}
 			}],
@@ -95,8 +92,7 @@ const (
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "meal choice",
-				"emoji": false
+				"text": "meal choice"
 			},
 			"blocks": [
 				{
@@ -104,8 +100,7 @@ const (
 					"block_id": "multi-line",
 					"label": {
 						"type": "plain_text",
-						"text": "dietary restrictions",
-				  		"emoji": false
+						"text": "dietary restrictions"
 					},
 					"element": {
 						"type": "plain_text_input",
@@ -118,8 +113,7 @@ const (
 					"block_id": "target_channel",
 					"label": {
 						"type": "plain_text",
-						"text": "Select a channel to post the result on",
-						"emoji": false
+						"text": "Select a channel to post the result on"
 					},
 					"element": {
 						"type": "conversations_select",
diff --git a/views_test.go b/views_test.go
index c86c0a0..5908737 100644
--- a/views_test.go
+++ b/views_test.go
@@ -767,8 +767,7 @@ func TestSlack_UpdateViewSubmissionResponse(t *testing.T) {
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "Test update view submission response",
-				"emoji": false
+				"text": "Test update view submission response"
 			},
 			"blocks": [
 				{
@@ -804,8 +803,7 @@ func TestSlack_PushViewSubmissionResponse(t *testing.T) {
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "Test update view submission response",
-				"emoji": false
+				"text": "Test update view submission response"
 			},
 			"blocks": [
 				{
@@ -814,8 +812,7 @@ func TestSlack_PushViewSubmissionResponse(t *testing.T) {
 					"elements": [
 						{
 							"type": "plain_text",
-							"text": "Context text",
-							"emoji": false
+							"text": "Context text"
 						},
 						{
 							"type": "image",
diff --git a/webhooks_test.go b/webhooks_test.go
index 4c76a1d..f369239 100644
--- a/webhooks_test.go
+++ b/webhooks_test.go
@@ -66,6 +66,25 @@ func TestPostWebhook_NotOK(t *testing.T) {
 	}
 }
 
+func TestPostWebhook_MessageLimitExceeded(t *testing.T) {
+	once.Do(startServer)
+
+	http.HandleFunc("/message_limit_exceeded", func(rw http.ResponseWriter, r *http.Request) {
+		// When a workspace's message limit is exceeded we get a 429 without a Retry-After header
+		rw.WriteHeader(http.StatusTooManyRequests)
+		rw.Write([]byte("message_limit_exceeded"))
+	})
+
+	url := "http://" + serverAddr + "/message_limit_exceeded"
+
+	err := PostWebhook(url, &WebhookMessage{})
+
+	if err == nil {
+		t.Errorf("Expected to receive error")
+	}
+	assert.IsType(t, StatusCodeError{}, err)
+}
+
 func TestWebhookMessage_WithBlocks(t *testing.T) {
 	textBlockObject := NewTextBlockObject("plain_text", "text", false, false)
 	sectionBlock := NewSectionBlock(textBlockObject, nil, nil)
@@ -77,13 +96,13 @@ func TestWebhookMessage_WithBlocks(t *testing.T) {
 	assert.Equal(t, 1, len(msgSingleBlock.Blocks.BlockSet))
 
 	msgJsonSingleBlock, _ := json.Marshal(msgSingleBlock)
-	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text","emoji":false}}],"replace_original":false,"delete_original":false}`, string(msgJsonSingleBlock))
+	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text"}}],"replace_original":false,"delete_original":false}`, string(msgJsonSingleBlock))
 
 	msgTwoBlocks := WebhookMessage{Blocks: twoBlocks}
 	assert.Equal(t, 2, len(msgTwoBlocks.Blocks.BlockSet))
 
 	msgJsonTwoBlocks, _ := json.Marshal(msgTwoBlocks)
-	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text","emoji":false}},{"type":"section","text":{"type":"plain_text","text":"text","emoji":false}}],"replace_original":false,"delete_original":false}`, string(msgJsonTwoBlocks))
+	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text"}},{"type":"section","text":{"type":"plain_text","text":"text"}}],"replace_original":false,"delete_original":false}`, string(msgJsonTwoBlocks))
 
 	msgNoBlocks := WebhookMessage{Text: "foo"}
 	msgJsonNoBlocks, _ := json.Marshal(msgNoBlocks)