socketmode

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 23, 2026 License: MIT Imports: 16 Imported by: 0

README

socketmode

Production-ready Socket Mode client for Slack, with type-safe responses.

Back to main documentation

Quick Start

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/pbotsaris/goblocks/blocks"
    "github.com/pbotsaris/goblocks/socketmode"
)

func main() {
    client := socketmode.New(os.Getenv("SLACK_APP_TOKEN"))

    client.OnSlashCommand(func(ctx context.Context, env *socketmode.Envelope) socketmode.Response {
        msg := blocks.NewBuilder().
            AddSection(blocks.MustMarkdown("Hello from */mycommand*!")).
            MustToMessage("Hello!")
        return socketmode.RespondWithMessage(msg)
    })

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    if err := client.Run(ctx); err != nil {
        log.Fatal(err)
    }
}

Handler Registration

// Convenience methods for common event types
client.OnSlashCommand(handler)  // slash_commands
client.OnInteractive(handler)   // interactive (buttons, modals, etc.)
client.OnEventsAPI(handler)     // events_api

// Generic handler for any event type
client.On("custom_event", handler)

The handler signature:

type EventHandler func(ctx context.Context, envelope *Envelope) Response

The Envelope contains:

type Envelope struct {
    EnvelopeID             string          // Unique ID for acknowledgment
    Type                   string          // Event type (events_api, interactive, etc.)
    Payload                json.RawMessage // Raw event payload
    AcceptsResponsePayload bool            // Whether response can include data
    RetryAttempt           int             // Retry attempt number (0 = first try)
    RetryReason            string          // Why this is a retry
}

Type-Safe Responses

Response builders integrate with the blocks package:

// Empty response (ack only)
socketmode.NoResponse()

// Message response (slash commands)
msg := blocks.NewBuilder().
    AddSection(blocks.MustMarkdown("*Result:* Success")).
    MustToMessage("Result")
socketmode.RespondWithMessage(msg)

// Quick message from blocks
socketmode.RespondWithBlocks([]blocks.Block{section, divider})

// Modal responses (view submissions)
socketmode.RespondWithModalUpdate(modal)  // Replace current modal
socketmode.RespondWithModalPush(modal)    // Push new modal onto stack
socketmode.RespondWithModalClear()        // Close all modals
socketmode.RespondWithErrors(map[string]string{
    "email_block": "Invalid email address",
})

// Dynamic options (external selects)
socketmode.RespondWithOptions([]blocks.Option{opt1, opt2})
socketmode.RespondWithOptionGroups([]blocks.OptionGroup{group1})

Client Options

client := socketmode.New(appToken,
    socketmode.WithLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))),
    socketmode.WithMetrics(&myMetrics{}),
    socketmode.WithMaxConcurrency(20),
    socketmode.WithHandlerTimeout(60 * time.Second),
    socketmode.WithHelloTimeout(30 * time.Second),
    socketmode.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)
Option Default Description
WithLogger slog.Default() Structured logger for debug/error output
WithMetrics NoopMetrics{} Metrics hook for observability
WithMaxConcurrency 10 Max concurrent handler goroutines
WithHandlerTimeout 30s Timeout for handler execution
WithHelloTimeout 30s Timeout waiting for hello message
WithHTTPClient Default client HTTP client for API calls

Colored Logging

The package includes a colored log handler for better readability during development:

// Use the built-in colored logger
logger := socketmode.NewColoredLogger()

// Or with a specific level
logger := socketmode.NewColoredLoggerWithLevel(slog.LevelDebug)

client := socketmode.New(token, socketmode.WithLogger(logger))

Output:

08:44:22 INFO  connection established app_id=A0AM77WSZ6W num_connections=1
08:44:33 WARN  reconnecting error="connection reset" attempt=1
08:45:00 ERROR handler failed error="timeout"
  • INFO in bold green
  • WARN in bold yellow
  • ERROR in bold red
  • DEBUG in dim gray
  • Attribute keys in cyan

For more control, use ColoredHandlerOptions:

handler := socketmode.NewColoredHandler(&socketmode.ColoredHandlerOptions{
    Level:      slog.LevelDebug,
    TimeFormat: "15:04:05.000",
    ShowDate:   true, // Include date in timestamp
})
logger := slog.New(handler)

Metrics & Observability

Implement MetricsHook to collect metrics:

type MetricsHook interface {
    ConnectionOpened(connID string)
    ConnectionClosed(connID string, duration time.Duration)
    ReconnectAttempt(attempt int, delay time.Duration)
    EnvelopeReceived(envType string)
    EnvelopeAcked(envType string, latency time.Duration)
    HandlerStarted(envType string)
    HandlerCompleted(envType string, duration time.Duration, err error)
    HandlerPanic(envType string, recovered any)
    WriteQueueDepth(depth int)
}

Example with Prometheus:

type PrometheusMetrics struct {
    envelopesReceived *prometheus.CounterVec
    ackLatency        *prometheus.HistogramVec
    handlerDuration   *prometheus.HistogramVec
}

func (m *PrometheusMetrics) EnvelopeReceived(envType string) {
    m.envelopesReceived.WithLabelValues(envType).Inc()
}

func (m *PrometheusMetrics) EnvelopeAcked(envType string, latency time.Duration) {
    m.ackLatency.WithLabelValues(envType).Observe(latency.Seconds())
}

func (m *PrometheusMetrics) HandlerCompleted(envType string, duration time.Duration, err error) {
    m.handlerDuration.WithLabelValues(envType).Observe(duration.Seconds())
}

Running with an HTTP Server

Run Socket Mode alongside an HTTP server using errgroup:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"

    "golang.org/x/sync/errgroup"
    "github.com/pbotsaris/goblocks/socketmode"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    client := socketmode.New(os.Getenv("SLACK_APP_TOKEN"))
    client.OnSlashCommand(handleSlashCommand)

    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    server := &http.Server{Addr: ":8080", Handler: mux}

    g, ctx := errgroup.WithContext(ctx)

    g.Go(func() error {
        return client.Run(ctx)
    })

    g.Go(func() error {
        return server.ListenAndServe()
    })

    g.Go(func() error {
        <-ctx.Done()
        return server.Shutdown(context.Background())
    })

    if err := g.Wait(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}

Error Handling

The client classifies errors automatically:

Permanent errors (stops reconnection):

  • Invalid authentication (invalid_auth)
  • Token revoked (token_revoked)
  • App uninstalled (app_uninstalled)
  • Socket Mode disabled (link_disabled)
  • HTTP 401, 403

Retryable errors (triggers reconnection with backoff):

  • Network timeouts
  • Connection refused
  • HTTP 429, 5xx
  • Rate limiting

Exponential backoff:

  • Base delay: 1 second
  • Max delay: 30 seconds
  • Jitter: 0-1 second
  • Resets after 60 seconds of stable connection

Panic Recovery

Handler panics are recovered automatically:

client.OnSlashCommand(func(ctx context.Context, env *socketmode.Envelope) socketmode.Response {
    panic("oops") // Recovered! Connection continues.
})

Complete Example: Modal Workflow

package main

import (
    "context"
    "encoding/json"
    "log/slog"
    "os"
    "os/signal"
    "syscall"

    "github.com/pbotsaris/goblocks/socketmode"
)

func main() {
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    client := socketmode.New(
        os.Getenv("SLACK_APP_TOKEN"),
        socketmode.WithLogger(logger),
    )

    client.OnInteractive(func(ctx context.Context, env *socketmode.Envelope) socketmode.Response {
        var payload struct {
            Type string `json:"type"`
        }
        if err := json.Unmarshal(env.Payload, &payload); err != nil {
            return socketmode.NoResponse()
        }

        switch payload.Type {
        case "view_submission":
            return handleViewSubmission(env)
        default:
            return socketmode.NoResponse()
        }
    })

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    if err := client.Run(ctx); err != nil {
        logger.Error("client stopped", "error", err)
    }
}

func handleViewSubmission(env *socketmode.Envelope) socketmode.Response {
    var submission struct {
        View struct {
            State struct {
                Values map[string]map[string]struct {
                    Value string `json:"value"`
                } `json:"values"`
            } `json:"state"`
        } `json:"view"`
    }

    if err := json.Unmarshal(env.Payload, &submission); err != nil {
        return socketmode.NoResponse()
    }

    email := submission.View.State.Values["email_block"]["email_input"].Value
    if email == "" {
        return socketmode.RespondWithErrors(map[string]string{
            "email_block": "Email is required",
        })
    }

    return socketmode.RespondWithModalClear()
}

Documentation

Index

Constants

View Source
const (
	EnvelopeTypeHello         = "hello"
	EnvelopeTypeDisconnect    = "disconnect"
	EnvelopeTypeEventsAPI     = "events_api"
	EnvelopeTypeInteractive   = "interactive"
	EnvelopeTypeSlashCommands = "slash_commands"
)

Envelope types sent by Slack.

View Source
const (
	DisconnectReasonLinkDisabled     = "link_disabled"
	DisconnectReasonWarning          = "warning"
	DisconnectReasonRefreshRequested = "refresh_requested"
)

Disconnect reasons sent by Slack.

Variables

View Source
var (
	ErrHelloTimeout     = errors.New("timeout waiting for hello message")
	ErrConnectionClosed = errors.New("connection closed")
	ErrWriteTimeout     = errors.New("write timeout")
	ErrShuttingDown     = errors.New("client is shutting down")
	ErrHandlerTimeout   = errors.New("handler timeout")
	ErrConcurrencyLimit = errors.New("concurrency limit reached")
)

Error sentinel values.

Functions

func ClassifyHTTPError

func ClassifyHTTPError(statusCode int) error

ClassifyHTTPError classifies an HTTP response status as permanent or retryable.

func ClassifyNetworkError

func ClassifyNetworkError(err error) error

ClassifyNetworkError classifies a network error as retryable.

func ClassifySlackError

func ClassifySlackError(slackError string) error

ClassifySlackError classifies a Slack API error as permanent or retryable.

func IsPermanentError

func IsPermanentError(err error) bool

IsPermanentError returns true if the error should not be retried.

func IsRetryableError

func IsRetryableError(err error) bool

IsRetryableError returns true if the error may succeed on retry.

func NewColoredLogger

func NewColoredLogger() *slog.Logger

NewColoredLogger creates a new slog.Logger with colored output. This is a convenience function for quick setup.

func NewColoredLoggerWithLevel

func NewColoredLoggerWithLevel(level slog.Level) *slog.Logger

NewColoredLoggerWithLevel creates a new colored logger with a specific level.

Types

type Ack

type Ack struct {
	EnvelopeID string `json:"envelope_id"`
	Payload    any    `json:"payload,omitempty"`
}

Ack is the acknowledgment sent back to Slack for each envelope.

type Backoff

type Backoff struct {
	// contains filtered or unexported fields
}

Backoff implements exponential backoff with jitter.

func NewBackoff

func NewBackoff(cfg BackoffConfig) *Backoff

NewBackoff creates a new Backoff with the given configuration.

func (*Backoff) Attempts

func (b *Backoff) Attempts() int

Attempts returns the current number of consecutive failed attempts.

func (*Backoff) CheckStable

func (b *Backoff) CheckStable() bool

CheckStable checks if the connection has been stable long enough to reset. Returns true if the backoff counter was reset.

func (*Backoff) MarkConnected

func (b *Backoff) MarkConnected()

MarkConnected should be called when a connection is successfully established. It records the time for stability tracking.

func (*Backoff) NextDelay

func (b *Backoff) NextDelay() time.Duration

NextDelay returns the next backoff delay and increments the attempt counter. Formula: min(baseDelay * 2^attempt + jitter, maxDelay)

func (*Backoff) Reset

func (b *Backoff) Reset()

Reset resets the attempt counter to zero.

type BackoffConfig

type BackoffConfig struct {
	BaseDelay  time.Duration // Initial delay (default: 1s)
	MaxDelay   time.Duration // Maximum delay (default: 30s)
	MaxJitter  time.Duration // Maximum random jitter (default: 1s)
	StableTime time.Duration // Time before resetting attempts (default: 60s)
}

BackoffConfig configures the backoff behavior.

func DefaultBackoffConfig

func DefaultBackoffConfig() BackoffConfig

DefaultBackoffConfig returns the default backoff configuration.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a Socket Mode client for Slack.

func New

func New(appToken string, opts ...Option) *Client

New creates a new Socket Mode client.

func (*Client) On

func (c *Client) On(eventType string, handler EventHandler)

On registers a handler for the given event type.

func (*Client) OnEventsAPI

func (c *Client) OnEventsAPI(handler EventHandler)

OnEventsAPI registers a handler for Events API events.

func (*Client) OnInteractive

func (c *Client) OnInteractive(handler EventHandler)

OnInteractive registers a handler for interactive events (buttons, modals, etc).

func (*Client) OnSlashCommand

func (c *Client) OnSlashCommand(handler EventHandler)

OnSlashCommand registers a handler for slash commands.

func (*Client) Run

func (c *Client) Run(ctx context.Context) error

Run starts the client and maintains a connection to Slack. It reconnects automatically on disconnection until ctx is cancelled. Returns a permanent error if the connection cannot be established.

type ColoredHandler

type ColoredHandler struct {
	// contains filtered or unexported fields
}

ColoredHandler is a slog.Handler that outputs colored, human-readable logs.

func NewColoredHandler

func NewColoredHandler(opts *ColoredHandlerOptions) *ColoredHandler

NewColoredHandler creates a new colored log handler. Writes to os.Stderr by default.

func NewColoredHandlerWithWriter

func NewColoredHandlerWithWriter(w io.Writer, opts *ColoredHandlerOptions) *ColoredHandler

NewColoredHandlerWithWriter creates a new colored log handler with a custom writer.

func (*ColoredHandler) Enabled

func (h *ColoredHandler) Enabled(_ context.Context, level slog.Level) bool

Enabled implements slog.Handler.

func (*ColoredHandler) Handle

func (h *ColoredHandler) Handle(_ context.Context, r slog.Record) error

Handle implements slog.Handler.

func (*ColoredHandler) WithAttrs

func (h *ColoredHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs implements slog.Handler.

func (*ColoredHandler) WithGroup

func (h *ColoredHandler) WithGroup(name string) slog.Handler

WithGroup implements slog.Handler.

type ColoredHandlerOptions

type ColoredHandlerOptions struct {
	// Level is the minimum level to log. Defaults to slog.LevelInfo.
	Level slog.Leveler
	// TimeFormat is the format for timestamps. Defaults to "15:04:05".
	TimeFormat string
	// ShowDate includes the date in timestamps. Defaults to false.
	ShowDate bool
}

ColoredHandlerOptions configures the colored handler.

type ConnectionInfo

type ConnectionInfo struct {
	AppID string `json:"app_id"`
}

ConnectionInfo contains app identification from the hello message.

type ConnectionOpenResponse

type ConnectionOpenResponse struct {
	OK    bool   `json:"ok"`
	URL   string `json:"url"`
	Error string `json:"error,omitempty"`
}

ConnectionOpenResponse is the response from apps.connections.open API.

type DebugInfo

type DebugInfo struct {
	Host                      string `json:"host"`
	Started                   string `json:"started"`
	BuildNumber               int    `json:"build_number"`
	ApproximateConnectionTime int    `json:"approximate_connection_time"`
}

DebugInfo contains connection debugging information from the hello message.

type DisconnectMessage

type DisconnectMessage struct {
	Type      string    `json:"type"`
	Reason    string    `json:"reason"`
	DebugInfo DebugInfo `json:"debug_info"`
}

DisconnectMessage is sent by Slack when requesting disconnection.

type EmptyResponse

type EmptyResponse struct{}

EmptyResponse represents an acknowledgment with no payload.

type Envelope

type Envelope struct {
	EnvelopeID             string          `json:"envelope_id"`
	Type                   string          `json:"type"`
	Payload                json.RawMessage `json:"payload"`
	AcceptsResponsePayload bool            `json:"accepts_response_payload"`
	RetryAttempt           int             `json:"retry_attempt,omitempty"`
	RetryReason            string          `json:"retry_reason,omitempty"`
}

Envelope wraps all messages received from Slack via Socket Mode.

type EventHandler

type EventHandler func(ctx context.Context, envelope *Envelope) Response

EventHandler processes an envelope and returns a type-safe response.

type HelloMessage

type HelloMessage struct {
	Type           string         `json:"type"`
	ConnectionInfo ConnectionInfo `json:"connection_info"`
	NumConnections int            `json:"num_connections"`
	DebugInfo      DebugInfo      `json:"debug_info"`
}

HelloMessage is sent by Slack upon successful WebSocket connection.

type MessageResponse

type MessageResponse struct {
	// contains filtered or unexported fields
}

MessageResponse responds with a message (for slash commands).

type MetricsHook

type MetricsHook interface {
	// ConnectionOpened is called when a WebSocket connection is established.
	ConnectionOpened(connID string)

	// ConnectionClosed is called when a WebSocket connection is closed.
	ConnectionClosed(connID string, duration time.Duration)

	// ReconnectAttempt is called before each reconnection attempt.
	ReconnectAttempt(attempt int, delay time.Duration)

	// EnvelopeReceived is called when an envelope is received from Slack.
	EnvelopeReceived(envType string)

	// EnvelopeAcked is called when an envelope acknowledgment is sent.
	EnvelopeAcked(envType string, latency time.Duration)

	// HandlerStarted is called when a handler begins processing.
	HandlerStarted(envType string)

	// HandlerCompleted is called when a handler finishes processing.
	HandlerCompleted(envType string, duration time.Duration, err error)

	// HandlerPanic is called when a handler panics.
	HandlerPanic(envType string, recovered any)

	// WriteQueueDepth is called periodically with the current write queue depth.
	WriteQueueDepth(depth int)
}

MetricsHook allows users to collect metrics from the socket mode client. Implement this interface and pass it via WithMetrics() to receive callbacks. All methods should be non-blocking and safe for concurrent use.

type ModalAction

type ModalAction int

ModalAction specifies what to do with a modal response.

const (
	// ModalActionUpdate replaces the current modal view.
	ModalActionUpdate ModalAction = iota
	// ModalActionPush pushes a new modal onto the stack.
	ModalActionPush
	// ModalActionClear closes all modal views.
	ModalActionClear
	// ModalActionErrors returns validation errors.
	ModalActionErrors
)

type ModalResponse

type ModalResponse struct {
	// contains filtered or unexported fields
}

ModalResponse responds with a modal action (for view submissions).

type NoopMetrics

type NoopMetrics struct{}

NoopMetrics is a no-op implementation of MetricsHook. Use this as a default when no metrics collection is needed.

func (*NoopMetrics) ConnectionClosed

func (n *NoopMetrics) ConnectionClosed(connID string, duration time.Duration)

func (*NoopMetrics) ConnectionOpened

func (n *NoopMetrics) ConnectionOpened(connID string)

func (*NoopMetrics) EnvelopeAcked

func (n *NoopMetrics) EnvelopeAcked(envType string, latency time.Duration)

func (*NoopMetrics) EnvelopeReceived

func (n *NoopMetrics) EnvelopeReceived(envType string)

func (*NoopMetrics) HandlerCompleted

func (n *NoopMetrics) HandlerCompleted(envType string, duration time.Duration, err error)

func (*NoopMetrics) HandlerPanic

func (n *NoopMetrics) HandlerPanic(envType string, recovered any)

func (*NoopMetrics) HandlerStarted

func (n *NoopMetrics) HandlerStarted(envType string)

func (*NoopMetrics) ReconnectAttempt

func (n *NoopMetrics) ReconnectAttempt(attempt int, delay time.Duration)

func (*NoopMetrics) WriteQueueDepth

func (n *NoopMetrics) WriteQueueDepth(depth int)

type Option

type Option func(*Client)

Option configures the Client.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient sets the HTTP client for API calls.

func WithHandlerTimeout

func WithHandlerTimeout(d time.Duration) Option

WithHandlerTimeout sets the timeout for handler execution.

func WithHelloTimeout

func WithHelloTimeout(d time.Duration) Option

WithHelloTimeout sets the timeout for waiting for the hello message.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for the client.

func WithMaxConcurrency

func WithMaxConcurrency(n int) Option

WithMaxConcurrency sets the maximum number of concurrent handlers.

func WithMetrics

func WithMetrics(hook MetricsHook) Option

WithMetrics sets the metrics hook for the client.

type OptionsResponse

type OptionsResponse struct {
	// contains filtered or unexported fields
}

OptionsResponse responds with options (for block_suggestion/dynamic menus).

type PermanentError

type PermanentError struct {
	Err     error
	Message string
}

PermanentError represents an error that should not be retried. Examples: invalid auth token, revoked app, app not installed.

func (*PermanentError) Error

func (e *PermanentError) Error() string

func (*PermanentError) Unwrap

func (e *PermanentError) Unwrap() error

type Response

type Response interface {
	// contains filtered or unexported methods
}

Response is the interface for all handler responses. The unexported method seals the interface to this package.

func NoResponse

func NoResponse() Response

NoResponse returns an empty response (ack only, no payload).

func RespondWithBlocks

func RespondWithBlocks(blks []blocks.Block) Response

RespondWithBlocks creates a message response from blocks directly. Convenience wrapper around RespondWithMessage.

func RespondWithErrors

func RespondWithErrors(errors map[string]string) Response

RespondWithErrors creates a response with validation errors. Use for view_submission when form validation fails. The errors map keys are block_ids, values are error messages.

func RespondWithMessage

func RespondWithMessage(msg blocks.Message) Response

RespondWithMessage creates a response with a message. Use for slash commands that accept response payloads.

func RespondWithModalClear

func RespondWithModalClear() Response

RespondWithModalClear creates a response that closes all modals. Use for view_submission when you want to dismiss the modal stack.

func RespondWithModalPush

func RespondWithModalPush(modal blocks.Modal) Response

RespondWithModalPush creates a response that pushes a new modal. Use for view_submission when you want to add a modal to the stack.

func RespondWithModalUpdate

func RespondWithModalUpdate(modal blocks.Modal) Response

RespondWithModalUpdate creates a response that updates the current modal. Use for view_submission when you want to replace the modal content.

func RespondWithOptionGroups

func RespondWithOptionGroups(groups []blocks.OptionGroup) Response

RespondWithOptionGroups creates a response with grouped options. Use for block_suggestion when populating an external select with groups.

func RespondWithOptions

func RespondWithOptions(opts []blocks.Option) Response

RespondWithOptions creates a response with options for dynamic menus. Use for block_suggestion when populating an external select.

type RetryableError

type RetryableError struct {
	Err     error
	Message string
}

RetryableError represents an error that may succeed on retry. Examples: network blip, rate limit, server error.

func (*RetryableError) Error

func (e *RetryableError) Error() string

func (*RetryableError) Unwrap

func (e *RetryableError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL