Slack Message Double HTML Encoding: & Appears Instead of &
When sending messages to Slack, ampersand characters are double-encoded due to pre-escaping in OpenClaw combined with Slack API escaping, resulting in & displaying instead of &.
π Symptoms
Visual Manifestation
When sending messages containing ampersands to Slack, users observe literal & text appearing in the channel instead of the expected & character.
Example Input:
Project: R&D Budget
Contact: John & Jane
Links: https://example.com/?a=1&b=2Expected Output in Slack:
Project: R&D Budget
Contact: John & Jane
Links: https://example.com/?a=1&b=2Actual (Broken) Output in Slack:
Project: R&D Budget
Contact: John & Jane
Links: https://example.com/?a=1&b=2Technical Error Sequence
The double-encoding occurs through the following pipeline:
- Input:
&(literal ampersand) - After OpenClaw escapeHtml():
& - After Slack API processing:
& - Rendered in Slack:
&(literal string)
Affected Message Patterns
This issue manifests in common scenarios:
- Company names:
R&D,B&B,P&G - Names:
John & Jane - URL query parameters:
?a=1&b=2 - Text with "and" alternatives:
Rock & Roll
π§ Root Cause
Architectural Analysis
The issue stems from a redundant encoding layer in the OpenClaw send module. The escapeHtml() function in dist/send-*.js pre-emptively escapes HTML entities before dispatching to the Slack API, which independently performs HTML entity encoding.
Code Flow Analysis
OpenClaw’s escapeHtml() implementation:
function escapeHtml(text) {
return text
.replace(/&/g, "&")
.replace(//g, ">");
}Slack API’s documented behavior:
Per Slack API documentation, incoming webhook payloads and API messages require & to be encoded as &. The Slack API performs this encoding server-side.
Double-Encoding Mechanism
The encoding pipeline operates as follows:
| Stage | Input | Operation | Output |
|---|---|---|---|
| 1. User Input | R&D | β | R&D |
| 2. OpenClaw escapeHtml() | R&D | Replace & β & | R&D |
| 3. Slack API | R&D | Replace & β & (within non-& contexts) | R&D |
| 4. Slack Render | R&D | Decode as literal text | R&D (visible) |
Why & Appears
The Slack API interprets the sequence as follows:
- The string contains
&(which Slack treats as an escaped ampersand) - Slack then escapes the
&within that sequence from&β& - This creates
&, which Slack displays as literal text rather than decoding as&
Affected Files
The problematic code exists in:
dist/send-slack.jsβ Slack-specific send moduledist/send-generic.jsβ Generic channel send module (may affect other integrations)- Potentially
src/send/slack.tsor equivalent source files
π οΈ Step-by-Step Fix
Option 1: Remove Pre-Escaping for Slack Channel (Recommended)
Modify the send module to bypass escapeHtml() specifically for Slack integration.
Before (problematic):
// In dist/send-slack.js
function buildPayload(message) {
return {
text: escapeHtml(message) // β Causes double-encoding
};
}After (fixed):
// In dist/send-slack.js
function buildPayload(message) {
return {
text: message // β
Let Slack handle encoding
};
}Option 2: Conditional Escaping with Detection
Implement smarter escaping that detects already-escaped sequences:
Proposed fix:
function escapeHtmlSmart(text) {
// Don't double-encode already-escaped sequences
return text
.replace(/&(?!amp;|lt;|gt;|quot;|#[0-9]+;)/g, "&")
.replace(//g, ">");
}Option 3: Environment-Aware Encoding
Add a configuration flag to control escaping behavior per channel:
Configuration addition:
// config/channels.json
{
"slack": {
"requiresHtmlEscape": false
},
"webhook": {
"requiresHtmlEscape": true
}
}Implementation:
function buildPayload(message, channelConfig) {
const text = channelConfig.requiresHtmlEscape
? escapeHtml(message)
: message;
return { text };
}Immediate Hotfix (Production)
If the issue requires immediate resolution without redeployment:
- Identify the affected message sources
- Temporarily replace `&` with a placeholder (e.g., `%26`) before OpenClaw processing
- Add a post-processing step after Slack receives to reverse the placeholder
Warning: This is a temporary measure only; implement a proper fix within 24-48 hours.
π§ͺ Verification
Test Case 1: Basic Ampersand
Command:
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-type: application/json' \
-d '{"text": "John & Jane"}'Expected Output in Slack: John & Jane
Acceptance Criteria: No & or & visible in the message
Test Case 2: Multiple Ampersands in URL
Command:
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-type: application/json' \
-d '{"text": "https://api.example.com?a=1&b=2&c=3"}'Expected Output in Slack: Full URL with properly rendered & characters
Test Case 3: Company Names with Ampersands
Command:
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-type: application/json' \
-d '{"text": "Partners: AT&T, P&G, B&M"}'Expected Output in Slack: AT&T, P&G, B&M rendered correctly
Automated Verification Script
#!/bin/bash
# test-slack-encoding.sh
TEST_CASES=(
"John & Jane"
"R&D Budget 2024"
"https://example.com/?x=1&y=2"
"AT&T | P&G | B&M"
)
WEBHOOK_URL="${SLACK_WEBHOOK_URL}"
for test in "${TEST_CASES[@]}"; do
echo "Testing: $test"
response=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$WEBHOOK_URL" \
-H 'Content-type: application/json' \
-d "{\"text\": \"$test\"}")
if [ "$response" == "200" ]; then
echo " β HTTP 200 - Message sent"
else
echo " β HTTP $response - Failed"
fi
doneRun verification:
chmod +x test-slack-encoding.sh
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" ./test-slack-encoding.shPost-Fix Validation Checklist
- β‘ No `&` strings appear in Slack messages
- β‘ No `&` sequences visible in message source
- β‘ URLs with query parameters render correctly
- β‘ Company names with `&` display correctly
- β‘ Other HTML entities (`<`, `>`) still render appropriately
- β‘ Exit code 0 from automated tests
β οΈ Common Pitfalls
1. Platform-Specific Encoding Differences
Pitfall: Removing escapeHtml() entirely may break other integrations that rely on pre-escaping.
Mitigation: Implement channel-specific encoding flags rather than global changes.
// β Wrong: Breaks other channels
// const escapeHtml = (text) => text.replace(...);
// β
Correct: Channel-aware escaping
function getEncoder(channel) {
const noEscape = ['slack', 'teams'];
return noEscape.includes(channel)
? (text) => text
: escapeHtml;
}2. Case Sensitivity in Entity Detection
Pitfall: The negative lookahead in Option 2 may miss uppercase variants.
Issue: Slack may receive or produce & or & in edge cases.
Mitigation: Normalize to lowercase before checking:
.replace(/&(?!amp;|lt;|gt;|quot;)/gi, "&")
// ^^ Add case-insensitive flag3. Nested or Partial Escaping
Pitfall: Text may contain partially-escaped content from prior processing.
Example: Input &Hello (already escaped) should remain &Hello, not become &amp;Hello.
Detection: Use regex to identify standalone & not followed by valid entity patterns.
4. Docker Container Caching
Pitfall: After fixing the source code, Docker containers may serve cached versions.
Resolution:
docker-compose down
docker-compose build --no-cache openclaw
docker-compose up -d5. Environment Variable Configuration
Pitfall: If using environment variables to control escaping behavior, ensure they propagate correctly in containerized environments.
Check:
docker exec -it openclaw-container env | grep HTML
# Should show: HTML_ESCAPE_REQUIRED=false6. Regression Testing Gaps
Pitfall: Fixing Slack encoding may inadvertently affect other channels (email, webhooks, etc.).
Mitigation: Run integration tests against all configured channels after any encoding-related changes.
π Related Errors
Directly Related
- SLACK-API-002: Block Kit Entity Encoding
When using Slack Block Kit with `mrkdwn` type blocks, additional escaping rules apply. Text in `mrkdwn` blocks may require different handling than plain `text` fields. - ESCAPE-001: Inconsistent Escape Behavior Across Channels
Different channel integrations (Slack, Teams, Email) have varying HTML encoding requirements, leading to inconsistent message rendering. - API-017: Webhook Payload Double-Encoding
Similar double-encoding issue reported for generic webhook integrations when both client and server apply HTML escaping.
Historically Related Issues
- HTML-INJECTION-003: Unescaped `<` and `>` in Slack Messages
Prior to the `escapeHtml()` addition, raw `<` and `>` characters could potentially inject HTML-like content. The fix introduced the ampersand double-encoding issue. - CHARACTER-SET-006: UTF-8 Encoding Issues with Special Characters
Unicode characters (emojis, accented letters) occasionally displayed incorrectly when passed through the encoding pipeline. - BUFFER-OVERFLOW-001: Long Messages Truncation
When `escapeHtml()` increased message length, messages exceeding Slack's 3001 character limit were silently truncated.