Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

_No unreleased changes at this time._
### Changed

- **`README.md`** — Expanded the rate-limiting section to name
`maxRateLimitRetries` explicitly, reworded the opening paragraph to
call out the zero-dependency WebSocket client and panic-recovered
dispatcher, added a dedicated "Vendored builds" subsection under
Installation, and extended the Security section with the
`maxFramePayload` guard and the `url.PathEscape` reaction-path fix.
- **`rest.go`** — Fixed the stale file-header comment that claimed 429
responses were "retried once"; the actual behaviour (3 retries,
bounded by `maxRateLimitRetries`) is now documented in the header.

---

Expand Down
44 changes: 35 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![CI](https://github.com/hilleywyn/godiscord/actions/workflows/ci.yml/badge.svg)](https://github.com/hilleywyn/godiscord/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

GoDiscord implements [Discord Gateway v10](https://discord.com/developers/docs/topics/gateway) and the Discord REST API v10 using only the Go standard library — no `github.com/gorilla/websocket`, no `github.com/bwmarrin/discordgo`, no external packages at all.
GoDiscord implements [Discord Gateway v10](https://discord.com/developers/docs/topics/gateway) and the Discord REST API v10 using only the Go standard library — no `github.com/gorilla/websocket`, no `github.com/bwmarrin/discordgo`, no external packages at all. It ships its own RFC 6455 WebSocket client and a typed event dispatcher, and every handler runs under panic recovery so one misbehaving callback can't take the bot down.

---

Expand Down Expand Up @@ -64,7 +64,22 @@ See [`example/basic/`](example/basic/) for a runnable starter bot and [`example/
go get github.com/hilleywyn/godiscord
```

GoDiscord requires **Go 1.21 or later**.
GoDiscord requires **Go 1.21 or later**. There are no transitive
dependencies: after `go get`, `go.sum` lists only GoDiscord itself.

### Vendored builds

For self-contained deployments (e.g. scratch Docker images), vendor
the module and build with `-mod=vendor`:

```bash
GOWORK=off go mod tidy
GOWORK=off go mod vendor
go build -mod=vendor ./...
```

The `GOWORK=off` disables workspace mode so a sibling `go.work` file
doesn't pull in live-dev paths during vendoring.

Comment on lines +72 to 83
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new “Vendored builds” subsection duplicates the later “Vendoring” section (both show the same GOWORK=off … go mod vendor workflow). To avoid the two sections drifting over time, consider consolidating into a single section or having one link to the other.

Suggested change
For self-contained deployments (e.g. scratch Docker images), vendor
the module and build with `-mod=vendor`:
```bash
GOWORK=off go mod tidy
GOWORK=off go mod vendor
go build -mod=vendor ./...
```
The `GOWORK=off` disables workspace mode so a sibling `go.work` file
doesn't pull in live-dev paths during vendoring.
For self-contained deployments (e.g. scratch Docker images), see
[Vendoring](#vendoring) for the recommended `go mod vendor` workflow
and `-mod=vendor` build instructions.

Copilot uses AI. Check for mistakes.
---

Expand Down Expand Up @@ -258,8 +273,9 @@ modPerms := discord.Permission(0).Add(
GoDiscord handles `429 Too Many Requests` responses automatically. When Discord
returns a rate-limit response the client reads the `Retry-After` header, sleeps
for the indicated duration, and retries the request. Retries are capped at
**3 attempts per call**; if the budget is exhausted a `*APIError` with
`StatusCode == 429` is returned so you can decide how to proceed.
**3 attempts per call** (`maxRateLimitRetries`); if the budget is exhausted a
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README says retries are capped at “3 attempts per call”, but the implementation allows up to 3 retries after the initial request (i.e. up to 4 total HTTP attempts). Consider rewording this to “up to 3 retries per call” (or “up to 4 attempts: 1 initial + 3 retries”) to match maxRateLimitRetries semantics in rest.go.

Suggested change
**3 attempts per call** (`maxRateLimitRetries`); if the budget is exhausted a
**up to 3 retries per call** (`maxRateLimitRetries`), for **up to 4 total
attempts** (1 initial request + 3 retries); if the budget is exhausted a

Copilot uses AI. Check for mistakes.
`*APIError` with `StatusCode == 429` is returned so you can decide how to
proceed.

```go
var apiErr *discord.APIError
Expand All @@ -268,6 +284,9 @@ if errors.As(err, &apiErr) && apiErr.IsRateLimit() {
}
```

The retry budget applies per-request, not per-process, so an occasional
429 on one endpoint doesn't starve the next call.

## Error Handling

REST calls return `*APIError` on failure, which can be inspected with `errors.As`:
Expand Down Expand Up @@ -356,7 +375,8 @@ intent** (`GuildMembers`, `GuildPresences`, `MessageContent`) is declared in
code but not enabled in the Developer Portal. Go to
[Discord Developer Portal](https://discord.com/developers/applications) →
your application → **Bot** → **Privileged Gateway Intents** and enable the
intents your bot requests.
intents your bot requests. GoDiscord surfaces the 4014 close code in the
gateway log so it's straightforward to recognise in traces.
Comment on lines +378 to +379
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This claims “GoDiscord surfaces the 4014 close code in the gateway log”, but the current WebSocket client treats close frames as io.EOF without parsing/logging the close code, and the gateway logs only a generic disconnect message. Either adjust the statement to reflect current logging, or update the gateway/websocket close handling to record the close code and reason.

Suggested change
intents your bot requests. GoDiscord surfaces the 4014 close code in the
gateway log so it's straightforward to recognise in traces.
intents your bot requests. If the bot connects and then immediately
disconnects after requesting one of these intents, verify that the matching
privileged intent is enabled in the portal.

Copilot uses AI. Check for mistakes.

### Messages are received but `m.Content` is always empty

Expand Down Expand Up @@ -391,10 +411,16 @@ GoDiscord is a framework library. Its security posture:
- **No SQL, no shell execution** — there is no injection surface beyond what
bot code introduces itself.
- **TLS only** — the Gateway and REST client connect exclusively over TLS.
- **Bounded allocation** — WebSocket frame payloads are capped at 64 MiB to
prevent memory-exhaustion attacks from a compromised gateway connection.
- **Bounded retries** — Rate-limit retries are capped to prevent infinite
recursion from a non-compliant server.
- **Bounded allocation** — WebSocket frame payloads are capped at 64 MiB
(`maxFramePayload`) to prevent memory-exhaustion attacks from a
compromised gateway connection. Negative payload lengths (8-byte length
field with its high bit set) are rejected before allocation.
- **Bounded retries** — Rate-limit retries are capped at
`maxRateLimitRetries` to prevent infinite recursion from a non-compliant
server.
- **Path-safe REST** — `AddReaction` and `RemoveReaction` pass the emoji
parameter through `url.PathEscape`, blocking path-injection via a
crafted emoji string.
- **Token isolation** — The bot token is stored in an unexported field and
never logged; it appears only in `Authorization` headers.

Expand Down
4 changes: 3 additions & 1 deletion rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ package discord
// - Failed requests return *APIError with the HTTP status and Discord JSON
// error code, so callers can branch with errors.As().
// - 429 Too Many Requests is handled transparently: the client sleeps for
// Retry-After seconds and retries once.
// Retry-After seconds and retries up to maxRateLimitRetries times, then
// returns an *APIError with StatusCode == 429 once the budget is
// exhausted so callers can back off at a higher level.
// - All public methods have descriptive godoc comments.

import (
Expand Down
Loading