Conversation
Move local development files to .local/ directory and update .gitignore to ignore the folder instead of individual files. This keeps local development artifacts out of the repository.
Replace [String:Int] decoding with SeqProbe struct to correctly extract sequence number from gateway dispatch frames. The previous implementation failed because real frames contain op, d (object), and t (string) fields, not just integers. This fixes RESUME functionality and heartbeat sequence tracking.
Include major parameters (channel_id, guild_id, webhook_id) in rate-limit bucket keys to properly isolate buckets per resource. Previous implementation lumped all channels/guilds into the same bucket, causing serialization of unrelated requests and 429 errors. Now uses format: METHOD:path|major=major_param
Add ensureChannelStub method to Cache that only inserts a stub channel when the key is missing. Update EventDispatcher to use this instead of upsert, preventing overwrites of full Channel objects with stubs that only contain id and type. This preserves channel metadata like name, topic, and permission_overwrites for permission checks.
Decode the boolean d field from INVALID_SESSION opcode to determine if the session is resumable. Only clear session state if d is false. If d is true, sleep 1-5 seconds and retry RESUME instead of unconditionally throwing away recoverable sessions.
Remove deprecated $ prefix from os, browser, and device keys in IdentifyConnectionProperties. Discord's current spec uses os, browser, and device without the $ prefix. The deprecated keys are tolerated but flagged as deprecated client by anti-spam systems.
Set maximumMessageSize to 16 MiB to handle large gateway payloads like GUILD_CREATE. The default 1 MB limit can cause socket closure with code 1009 for large guilds with many members, channels, roles, threads, and stickers.
Ensure URLError.cancelled is checked before retry logic in the outer catch block. Cancellation should always be terminal and should not trigger retry attempts. The check now happens before the attempt < maxAttempts check.
Add disconnected(reason:) event to DiscordEvent to surface fatal disconnects. Add closeCode property to WebSocketClient protocol and implement it in adapters. Check for fatal 4000-series close codes before attempting reconnection and surface descriptive errors. Stop reconnection on fatal codes like 4004 (auth failed), 4011 (sharding required), 4013/4014 (invalid intents). Surface disconnected event when max reconnect attempts are reached.
Add resume_gateway_url field to ReadyEvent and store it in GatewayClient. Use the resume URL when connecting instead of the default gateway URL. This is required by Discord for v10 resumes and reduces latency by avoiding redirects.
Mark zlib-stream and zstd-stream compression options as unavailable until decompression is implemented. Enabling these currently silently breaks the bot since the readLoop only handles string and data frames as raw JSON, not compressed binary frames.
Move attachment descriptors into payload_json instead of creating separate multipart parts per file. Discord expects a single attachments JSON array embedded in the payload_json part, not multiple attachments form-data parts. This fixes the issue where multiple files with descriptions would create duplicate multipart parts that Discord rejects.
Fix four compilation errors introduced in v2.3 correctness patch: 1. Add disconnected case to EventDispatcher switch for exhaustiveness 2. Fix resume_gateway_url String? to URL conversion 3. Replace non-existent attemptResume with attemptReconnect 4. Remove optional chaining from non-optional URLSessionWebSocketTask.closeCode
Remove stray code after actor closing brace and remove non-existent onDisconnect callback. The disconnected case now simply breaks to satisfy switch exhaustiveness without calling undefined properties.
The prior syntax-fix commit restored the actor's closing brace and the func's closing brace but left the switch statement unterminated, so all three CI builds (macOS, Ubuntu, Windows) failed with cascading 'expected ''}''' errors and spurious 'EventDispatcher has no member ''process''' diagnostics in DiscordClient.swift. Add the missing '}' after the final '.disconnected' case so the switch, func, and actor all close correctly, restoring a parsable module and resolving every downstream error in the CI logs.
Bump version to 2.3.0 in DiscordConfiguration.swift and update CHANGELOG.md with comprehensive fix details including EventDispatcher syntax corrections, Swift 6.2 compilation fixes, multipart attachment handling, gateway connection improvements, and WebSocket reliability enhancements. Update README.md to reference version 2.3.0 and document debugging capabilities including gateway decode diagnostics, rate limit observability, typed error handling, router error handlers, and default error logging.
Update isFatalCloseCode to return true only for explicit fatal codes (4004, 4010, 4011, 4012, 4013, 4014) instead of treating the entire 4000-4999 range as fatal. This allows reconnection attempts for recoverable close codes like 4000, 4007, 4008, 4009 while still blocking reconnection for truly fatal authentication, sharding, and intent errors.
Fixed two P0 rate-limiting issues: 1. Bucket key generation now preserves major parameters (channel_id, guild_id, webhook_id) instead of replacing all snowflakes with :id. This ensures each channel/guild gets its own bucket, preventing unrelated requests from being serialized together. 2. Added per-bucket AsyncSemaphore to limit concurrent requests before bucket limits are known from headers. Defaults to 50 concurrent requests per bucket (Discord's typical limit) and updates dynamically based on X-RateLimit-Limit headers. These fixes prevent 429 errors in multi-guild scenarios by ensuring proper bucket isolation and request serialization. Resolves P0 bugs §1.3 and §1.11 from developer analysis.
Added optional reason parameter to all HTTPClient methods for audit log tracking on destructive operations (bans, kicks, deletions, etc.). The reason is URL-encoded and sent via the X-Audit-Log-Reason header as specified by Discord's API. This allows bot developers to provide context for moderation actions in the guild audit log, improving accountability and debugging capabilities. Breaking change: All HTTPClient method signatures now include an optional reason parameter. This is backward compatible since the parameter has a default value of nil. Resolves P1 feature §2.11 from developer analysis.
Implemented POST /guilds/{id}/bulk-ban endpoint supporting up to
200 user IDs per call. This is a 2024 Discord API endpoint that
significantly improves efficiency when banning multiple users.
The new bulkBanMembers method:
- Accepts an array of up to 200 user IDs
- Supports delete_message_seconds parameter (0-604800)
- Uses the new X-Audit-Log-Reason header for audit logging
- Returns BulkBanResponse with list of successfully banned users
- Validates the 200-user limit before making the request
Added BulkBanResponse model to GuildBan.swift with banned_users
array containing the IDs of users that were successfully banned.
This resolves P1 feature §2.15 from developer analysis.
Updated searchGuildMembers to use the 2024+ POST endpoint
/guilds/{id}/members-search instead of the deprecated GET
endpoint with query parameter.
The new POST endpoint:
- Uses request body instead of query parameters
- More consistent with Discord's modern API patterns
- Replaces the old GET /guilds/{id}/members/search?query=
This resolves P1 feature §2.4 from developer analysis.
Implemented GET /channels/{id}/polls/{message.id}/answers/{answer_id}
endpoint for paginated retrieval of users who voted for a specific
poll answer.
The new getPollAnswerVoters method:
- Accepts channelId, messageId, and answerId parameters
- Supports pagination via 'after' user ID and 'limit' (1-100)
- Returns PollAnswerUsers with user list and pagination metadata
- Default limit is 25 users per page
Added PollAnswerUsers model to Message.swift containing:
- has_more: Boolean indicating if more users exist
- users: Array of User objects who voted
- after: User ID for next pagination request
This enables efficient retrieval of poll voters for large polls
without fetching all data at once.
Resolves P1 feature §2.5 from developer analysis.
Updated application emoji methods to use correct Discord API path
/applications/{id}/emojis instead of the incorrect /app-emojis.
Changes:
- Renamed createAppEmoji to createApplicationEmoji (breaking change)
- Renamed updateAppEmoji to updateApplicationEmoji (breaking change)
- Renamed deleteAppEmoji to deleteApplicationEmoji (breaking change)
- Added listApplicationEmojis method to fetch all app emojis
- Changed return types from JSONValue to proper Emoji types
- Added typed parameters (name, roles) instead of generic JSONValue
- Changed emojiId parameter type from String to EmojiID
The application emoji endpoints allow bots to create and manage
emojis that are usable across all guilds, not just within
a single guild.
Resolves P1 feature §2.3 from developer analysis.
Fixed createGuildSticker to use Discord's specific multipart format with individual form-data fields (name, description, tags, file) instead of the standard payload_json shape. Added postStickerMultipart method to HTTPClient that builds multipart body with Discord's exact field layout: - name: form-data field for sticker name - description: optional form-data field for sticker description - tags: form-data field for comma-separated tag string - file: form-data field with the sticker image file Updated createGuildSticker to use the new method and added support for audit log reason parameter. This fixes sticker uploads which were using the wrong format and would be rejected by Discord's API. Resolves P1 feature §2.14 from developer analysis.
Implemented POST /channels/{id}/send-soundboard-sound endpoint
for playing soundboard sounds in voice channels.
The new playSoundboardSound method:
- Accepts channelId (voice channel), soundId, and guildId
- Sends POST request with sound_id and guild_id in body
- Returns the SoundboardSound object
- Enables bots to play soundboard sounds programmatically
This complements the existing soundboard management methods
(list, create, modify, delete) by adding the ability to actually
play sounds in voice channels.
Resolves P1 feature §2.16 from developer analysis.
Added name_localizations and description_localizations fields to ApplicationCommandCreate and ApplicationCommandOption structs to support localization during command creation. Changes: - Added name_localizations: [String: String]? to ApplicationCommandCreate - Added description_localizations: [String: String]? to ApplicationCommandCreate - Added name_localizations: [String: String]? to ApplicationCommandOption - Added description_localizations: [String: String]? to ApplicationCommandOption - Added name_localizations: [String: String]? to ApplicationCommandOption.Choice The setCommandLocalizations method already existed for updating localizations after creation, but this allows setting localizations during initial command creation, which is more efficient and follows Discord's recommended pattern. This enables bots to provide localized command names, descriptions, and option names for different locales (e.g., en-US, es-ES, fr-FR). Resolves P1 feature §2.2 from developer analysis.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds per-route semaphores, threads optional audit ChangesHTTP Client and REST API Enhancements
Sequence Diagram(s)sequenceDiagram
participant Caller
participant HTTPClient
participant applyCustomHeaders
participant DiscordAPI
Caller->>HTTPClient: post(path, body, reason: "violation")
HTTPClient->>applyCustomHeaders: applyCustomHeaders(headers, auditReason: reason)
applyCustomHeaders->>applyCustomHeaders: URL-encode reason
applyCustomHeaders-->>HTTPClient: X-Audit-Log-Reason header set
HTTPClient->>DiscordAPI: POST with audit header
DiscordAPI-->>HTTPClient: response
HTTPClient-->>Caller: return response
sequenceDiagram
participant HTTPClient
participant BucketSemaphores
participant AsyncSemaphore
participant DiscordAPI
HTTPClient->>BucketSemaphores: get(for: routeKey)
BucketSemaphores->>AsyncSemaphore: get or create semaphore
HTTPClient->>AsyncSemaphore: wait()
AsyncSemaphore-->>HTTPClient: acquired permit (or await)
HTTPClient->>DiscordAPI: execute request
DiscordAPI-->>HTTPClient: response + X-RateLimit-Limit
HTTPClient->>BucketSemaphores: updatePermits(limit)
HTTPClient->>AsyncSemaphore: signal() via defer
AsyncSemaphore-->>HTTPClient: release permit
HTTPClient-->>HTTPClient: return response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: M1tsumi <88093506+M1tsumi@users.noreply.github.com>
The previous code tolerated three consecutive missed ACKs before reconnecting. Discord's gateway spec expects a single missed heartbeat ACK to be treated as a zombied connection. Change the threshold from >= 3 to >= 1 so the client reconnects and resumes immediately after the first unacknowledged heartbeat, matching spec behavior.
Discord invalidates the session when a client sends close code 1000 or 1001. The previous attemptReconnect always called socket.close, which sent a normal closure. This forced a fresh identify on every reconnect instead of resuming. Add forceClose to WebSocketClient protocol and implementations. forceClose tears down the underlying connection without sending a clean close frame. Update attemptReconnect to use forceClose so Discord preserves the session_id for resume. Keep close for intentional user disconnects.
Discord limits each gateway connection to 120 sends per 60 seconds. Add a token-bucket rate limiter actor (GatewaySendRateLimiter) that refills at 2 tokens per second with a max of 120 tokens. Introduce sendGatewayPayload and sendGatewayData helpers inside GatewayClient so every outgoing payload goes through the limiter. This covers Identify, Resume, Heartbeat, Presence Update, and Request Guild Members. Heartbeats remain unaffected in practice because they occur far below the limit, but they are now protected from accidental bursts alongside other sends.
Discord rejects identify payloads sent faster than once per 5 seconds per token. Add lastIdentifyAt tracking inside GatewayClient and sleep for the remaining time before sending a fresh Identify. This prevents 4008 (rate limited) or 4013 (invalid intents) style rejections caused by rapid reconnects and keeps the bot compliant with gateway session start limits.
Discord recommends fetching the gateway URL via GET /gateway/bot rather than hardcoding wss://gateway.discord.gg. Add GatewayBotResponse and SessionStartLimit models, implement fetchGatewayBot inside GatewayClient, and cache the returned URL in cachedGatewayUrl. On connect, if no resume_gateway_url or cached URL exists, fetch from REST and fall back to the configuration default on failure. The cached URL is reused across reconnects, while resume_gateway_url from READY still takes precedence for resumable sessions.
Added guard check to prevent crash when charset is empty. Returns empty string instead of crashing, following defensive programming practices similar to the Seyfert library issue.
Build compiles cleanly and all 34 tests pass.
Adds disconnect alias, initial presence support, DiscordError.authenticationFailed, and fatal gateway close code propagation to the v2.3.1 changelog.
Add JSONCoders for shared encoder/decoder instances across the library. Add OptionalField enum to distinguish absent, null, and value states for PATCH payloads. Add RetryPolicy for configurable transient failure retry behavior. Add RedactedToken wrapper to prevent token leakage in logs. Add DiscordAPIErrorBody to parse Discord's nested validation error responses. Add comprehensive unit tests in InternalTests.swift.
Add apiValidation case to handle Discord's nested validation error responses (e.g., Invalid Form Body). Add convenience properties: httpStatusCode, apiErrorCode, validationErrors, isRateLimited, isAuthenticationFailure, isCancelled, isTransient. These provide easy access to error metadata without pattern matching.
Replace local JSONEncoder/JSONDecoder instances with shared JSONCoders.encoder/decoder. Integrate RetryPolicy for configurable transient failure retry behavior. Add makeAPIError helper to parse Discord API errors including nested validation errors using DiscordAPIErrorBody. Use RetryPolicy for exponential backoff on network errors, cancellations, and 5xx responses.
Add proactive global rate limiting using a sliding 1-second window of 50 requests per second to prevent unnecessary 429s during cross-route bursts. Implement Discord bucket ID tracking with routeKeyToBucket mapping for accurate per-bucket state management. Add scope detection (user/global/shared) to prevent shared-scope limits from promoting to global reset. Add fallback to X-RateLimit-Reset epoch timestamp header.
Add messageToChannelIndex reverse index for O(1) message removal instead of O(n) channel scan. Update add(message:) to maintain reverse index and clean up removed messages when cap is exceeded. Update removeMessage(id:) to use reverse index for efficient lookup. Add removeGuild(id:) method to clear guild entry, roles, emojis, and associated data from cache.
Replace token String with RedactedToken to prevent accidental leakage in logs and errors. Replace local JSONEncoder/JSONDecoder with shared JSONCoders.encoder/decoder. Use token.rawValue for identify/resume payloads and token.authorizationHeaderValue for Authorization header. Update User-Agent header to DiscordBot format with repository URL.
Replace local JSONEncoder/JSONDecoder instances with shared JSONCoders.encoder/decoder. This ensures webhook payloads round-trip with the same key strategy as the rest of the library. The previous local coders applied .convertFromSnakeCase which silently broke Message decoding since models declare snake_case property names directly.
Change content parameter from String? to OptionalField<String> in DiscordClient.editMessage and editMessageWithFiles. Update MessagePayload.content to OptionalField<String> with default .absent. Add clearContent() method to explicitly send null to Discord. Add custom encode(to:) implementations to handle three-state encoding. This allows distinguishing between absent (do not change), null (clear field), and value (set field).
Add comprehensive documentation for Mentions, EmojiUtils, TimestampStyle, DiscordTimestamp, and MessageFormat enums. Fix MessageFormat.escapeSpecialCharacters() to only escape Discord's actual markdown metacharacters (\\, *, _, ~, |, >, ) and remove unnecessary escapes for parentheses, brackets, braces, plus, minus, equals, period, exclamation mark.
Call cache.removeGuild(id:) in EventDispatcher when handling guildDelete events. This ensures guild data, roles, emojis, and associated cache entries are cleaned up when a guild is deleted or the bot leaves a guild.
Add changelog entry for version 2.4.0 documenting internal utility infrastructure improvements including JSONCoders, OptionalField, RetryPolicy, RedactedToken, DiscordAPIErrorBody, cache optimizations, DiscordError enhancements, and various bug fixes.
Merge new internal utility infrastructure changes (JSONCoders, OptionalField, RetryPolicy, RedactedToken, DiscordAPIErrorBody, cache optimizations, DiscordError enhancements) into the existing 2.3.1 release entry instead of creating a new 2.4.0 version since 2.4.0 has not been released and is not planned.
Summary by CodeRabbit
New Features
Improvements
Chores