Repository: ably/ably-go
Pull request #684 (2025-07-08 10:50:41)

feat: Update the hasMsgSerial flag
This PR has two changes:

1. The hasMsgSerial flag now includes `actionObject` protocol message actions. 

This means that LiveObject messages have their message serials advanced when publishing. This was causing the connection to be disconnected and re-established by the server because when publishing multiple separated protocol messages with an `actionObject` Action type the messages where not monotonic and the server would error and disconnect. This had a further side effect that websocket go-routines where being leaked.

2. Improve logging around messages

Messages Received on a connection with `Disconnected` action, amongst others, may contain an error. There is now a check if an error is set if so it is logged as an Error. 

The `ProtocolMessage.String()` method now captures the errors in the Messages that may have an error set. Which messages maybe Errors set is based on a look in the Realtime codebase.



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved logging to clearly differentiate error messages from regular messages.
  * Enhanced internal handling to ensure serial numbers are correctly assigned to additional message types.

* **New Features**
  * Error details are now included in status messages for certain connection and channel actions, providing clearer information when issues occur.
  * Added detailed string representations for error information to improve error visibility and debugging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->

> [!NOTE]
> Currently processing new changes in this PR. This may take a few minutes, please wait...
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between cb3491f3ff7696f7fd486f368f16cca1bcff3f91 and 5f2f72abf0e80e14162d0615ae3d178e5e295644.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (1)</summary>
> 
> * `ably/realtime_conn.go` (1 hunks)
> 
> </details>
> 
> ```ascii
>  __________________________________________________________
> < Keep your friends close, but your code reviewers closer. >
>  ----------------------------------------------------------
>   \
>    \   (\__/)
>        (•ㅅ•)
>        / 　 づ
> ```

<!-- end of auto-generated comment: review in progress by coderabbit.ai -->
<!-- usage_tips_start -->

> [!TIP]
> <details>
> <summary>CodeRabbit can use Shopify Theme Check to improve the quality of Shopify theme reviews.</summary>
> 
> Add a [configuration file](https://shopify.dev/docs/themes/tools/theme-check) to your project to customize how CodeRabbit runs Shopify Theme Check.
> 
> </details>

<!-- usage_tips_end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings
<details open="true">
<summary>🧪 Generate unit tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post copyable unit tests in a comment
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit unit tests in branch `feature/pub-1655-advance-msgserial-for-object-messages`

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ably/ably-go&utm_content=684):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai auto-generate unit tests` to generate unit tests for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/ably/proto_error.go b/ably/proto_error.go
index 105d46f..16f5fc2 100644
--- a/ably/proto_error.go
+++ b/ably/proto_error.go
@@ -1,5 +1,10 @@
 package ably
 
+import (
+	"strconv"
+	"strings"
+)
+
 // errorInfo describes an error object returned via ProtocolMessage.
 type errorInfo struct {
 	StatusCode int    `json:"statusCode,omitempty" codec:"statusCode,omitempty"`
@@ -26,3 +31,53 @@ func (e *errorInfo) FromMap(ctx map[string]interface{}) {
 		e.Server = v.(string)
 	}
 }
+
+func (e *errorInfo) String() string {
+	if e == nil {
+		return "(nil)"
+	}
+
+	const (
+		labelMsg        = "msg="
+		labelCode       = "Code="
+		labelStatusCode = "StatusCode="
+		labelHref       = "Href="
+		labelServer     = "Server="
+	)
+
+	size := len("(") + len(labelMsg) + len(e.Message) +
+		len(","+labelCode) + len(strconv.Itoa(e.Code)) +
+		len(","+labelStatusCode) + len(strconv.Itoa(e.StatusCode)) + len(")")
+
+	if e.HRef != "" {
+		size += len(","+labelHref) + len(e.HRef)
+	}
+
+	if e.Server != "" {
+		size += len(","+labelServer) + len(e.Server)
+	}
+
+	var b strings.Builder
+	b.Grow(size)
+
+	b.WriteString("(" + labelMsg)
+	b.WriteString(e.Message)
+	b.WriteString("," + labelCode)
+	b.WriteString(strconv.Itoa(e.Code))
+	b.WriteString("," + labelStatusCode)
+	b.WriteString(strconv.Itoa(e.StatusCode))
+
+	if e.HRef != "" {
+		b.WriteString("," + labelHref)
+		b.WriteString(e.HRef)
+	}
+
+	if e.Server != "" {
+		b.WriteString("," + labelServer)
+		b.WriteString(e.Server)
+	}
+
+	b.WriteString(")")
+
+	return b.String()
+}
diff --git a/ably/proto_protocol_message.go b/ably/proto_protocol_message.go
index 5cce4e0..809d211 100644
--- a/ably/proto_protocol_message.go
+++ b/ably/proto_protocol_message.go
@@ -162,26 +162,26 @@ func (msg *protocolMessage) String() string {
 	case actionConnect:
 		return fmt.Sprintf("(action=%q)", msg.Action)
 	case actionConnected:
-		return fmt.Sprintf("(action=%q, id=%q, details=%#v)", msg.Action, msg.ConnectionID, msg.ConnectionDetails)
+		return fmt.Sprintf("(action=%q, id=%q, details=%#v, error=%s)", msg.Action, msg.ConnectionID, msg.ConnectionDetails, msg.Error)
 	case actionDisconnect:
 		return fmt.Sprintf("(action=%q)", msg.Action)
 	case actionDisconnected:
-		return fmt.Sprintf("(action=%q)", msg.Action)
+		return fmt.Sprintf("(action=%q, error=%s)", msg.Action, msg.Error)
 	case actionClose:
 		return fmt.Sprintf("(action=%q)", msg.Action)
 	case actionClosed:
 		return fmt.Sprintf("(action=%q)", msg.Action)
 	case actionError:
-		return fmt.Sprintf("(action=%q, error=%#v)", msg.Action, msg.Error)
+		return fmt.Sprintf("(action=%q, error=%s)", msg.Action, msg.Error)
 	case actionAttach:
 		return fmt.Sprintf("(action=%q, channel=%q)", msg.Action, msg.Channel)
 	case actionAttached:
-		return fmt.Sprintf("(action=%q, channel=%q, channelSerial=%q, flags=%x)",
-			msg.Action, msg.Channel, msg.ChannelSerial, msg.Flags)
+		return fmt.Sprintf("(action=%q, channel=%q, channelSerial=%q, flags=%x, error=%s)",
+			msg.Action, msg.Channel, msg.ChannelSerial, msg.Flags, msg.Error)
 	case actionDetach:
 		return fmt.Sprintf("(action=%q, channel=%q)", msg.Action, msg.Channel)
 	case actionDetached:
-		return fmt.Sprintf("(action=%q, channel=%q)", msg.Action, msg.Channel)
+		return fmt.Sprintf("(action=%q, channel=%q, error=%s)", msg.Action, msg.Channel, msg.Error)
 	case actionPresence, actionSync:
 		return fmt.Sprintf("(action=%q, id=%v, channel=%q, timestamp=%d, connectionId=%v, presenceMessages=%v)",
 			msg.Action, msg.ID, msg.Channel, msg.Timestamp, msg.ConnectionID, msg.Presence)
diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go
index 284ee0b..026f46f 100644
--- a/ably/realtime_conn.go
+++ b/ably/realtime_conn.go
@@ -617,7 +617,7 @@ func (c *Connection) advanceSerial() {
 }
 
 func (c *Connection) send(msg *protocolMessage, onAck func(err error)) {
-	hasMsgSerial := msg.Action == actionMessage || msg.Action == actionPresence
+	hasMsgSerial := msg.Action == actionMessage || msg.Action == actionPresence || msg.Action == actionObject
 	c.mtx.Lock()
 	// RTP16a - in case of presence msg send, check for connection status and send accordingly
 	switch state := c.state; state {
@@ -1057,7 +1057,12 @@ func (vc verboseConn) Receive(deadline time.Time) (*protocolMessage, error) {
 	if err != nil {
 		return nil, err
 	}
-	vc.logger.Verbosef("Realtime Connection: received %s", msg)
+
+	if msg.Error != nil {
+		vc.logger.Errorf("Realtime Connection: error received %s", msg)
+	} else {
+		vc.logger.Verbosef("Realtime Connection: received %s", msg)
+	}
 	return msg, nil
 }
 


Test patch:
diff --git a/ably/proto_error_test.go b/ably/proto_error_test.go
new file mode 100644
index 0000000..cf0aa04
--- /dev/null
+++ b/ably/proto_error_test.go
@@ -0,0 +1,64 @@
+package ably
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func Test_errorInfo_String(t *testing.T) {
+	tests := []struct {
+		name           string
+		err            *errorInfo
+		expectedString string
+		expectedSize   int
+	}{
+		{
+			name: "error with all fields",
+			err: &errorInfo{
+				StatusCode: 400,
+				Code:       40012,
+				HRef:       "ably.com/errors/40012",
+				Message:    "something went wrong",
+				Server:     "s1",
+			},
+			expectedString: "(msg=something went wrong,Code=40012,StatusCode=400,Href=ably.com/errors/40012,Server=s1)",
+			expectedSize:   89,
+		},
+		{
+			name: "error with no href",
+			err: &errorInfo{
+				StatusCode: 400,
+				Code:       40012,
+				Message:    "something went wrong",
+				Server:     "s1",
+			},
+			expectedString: "(msg=something went wrong,Code=40012,StatusCode=400,Server=s1)",
+			expectedSize:   62,
+		},
+		{
+			name: "error with no server",
+			err: &errorInfo{
+				StatusCode: 400,
+				Code:       40012,
+				HRef:       "ably.com/errors/40012",
+				Message:    "something went wrong",
+			},
+			expectedString: "(msg=something went wrong,Code=40012,StatusCode=400,Href=ably.com/errors/40012)",
+			expectedSize:   79,
+		},
+		{
+			name:           "nil error",
+			err:            nil,
+			expectedString: "(nil)",
+			expectedSize:   5,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := tt.err.String()
+
+			assert.Equal(t, tt.expectedString, s)
+			assert.Equal(t, tt.expectedSize, len(s))
+		})
+	}
+}