Skip to content

feat: discord_errors() middleware + store.mutate migration (server_stats) - #130

Merged
rolling-codes merged 2 commits into
mainfrom
feat/plugin-ergonomics
Aug 11, 2026
Merged

feat: discord_errors() middleware + store.mutate migration (server_stats)#130
rolling-codes merged 2 commits into
mainfrom
feat/plugin-ergonomics

Conversation

@rolling-codes

@rolling-codes rolling-codes commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

Two related plugin-ergonomics improvements, grounded in what the framework already provides:

  1. discord_errors() middleware (easycord/middleware.py) — catches discord.Forbidden / NotFound / HTTPException raised while running a command and replies with a localized, type-specific ephemeral message. It's the central, type-aware complement to the existing catch_errors() (a generic catch-all) and the existing @command_error (per-command). Register it after catch_errors() so it handles Discord errors first and catch_errors() remains the outer fallback; non-Discord errors propagate untouched.

  2. server_stats config writes → store.mutate — replaces the manual GuildLockManager + load/save ceremony with ServerConfigStore.mutate, consolidating on the single lock domain the store already owns. Behavior-preserving; removes the now-unused _locks.

Why this shape (not a decorator / not a bulk refactor)

  • @safe_discord was considered and rejected: EasyCord already has @command_error("cmd") for per-command type-specific handling and catch_errors() for the safety net. A new execution-wrapping decorator would duplicate @command_error and risk breaking slash-parameter introspection. A middleware fits the framework's model, needs one registration, and carries no introspection risk.
  • The config migration is behavior-preserving, not a bug fix: I verified empirically that the old separate-lock pattern does not lose updates (the store's load/save wrap synchronous I/O and never suspend between them). store.mutate is a simplification/consistency win.

Scope note (server_stats only)

server_stats is included as a verified reference migration. The remaining plugins that still use the manual pattern (auto_role, birthday, giveaway, polls, reminder, reputation, scheduled_announcements, tickets, verification, word_filter) are not in this PR: the migration is not mechanical — several interleave early returns and post-lock Discord I/O with their mutation (which can't move into mutate's synchronous callback verbatim), and each plugin's tests couple to _locks and need updating too. Recommend doing them incrementally, one verified plugin per change, rather than a risky bulk pass.

Test plan

  • discord_errors(): 6 new tests (tests/test_discord_errors_middleware.py) — per-type messages, custom overrides, non-Discord errors propagate, success path silent.
  • server_stats: existing suite updated and green (19 tests).
  • Full suite: 1843 passed, no regressions.
  • ruff check ... --select E9,F63,F7,F82 clean; pyright clean on changed files.

Summary by Sourcery

Introduce middleware to translate Discord API errors into user-friendly command responses and simplify server_stats configuration updates by using the shared config store mutation API.

New Features:

  • Add discord_errors() middleware to centrally handle common Discord API errors with localized ephemeral replies.

Enhancements:

  • Refactor server_stats plugin to perform configuration changes via ServerConfigStore.mutate instead of manual lock/load/save management.
  • Remove the plugin-specific GuildLockManager from server_stats in favor of the store’s existing locking domain.

Tests:

  • Add a dedicated test suite for the discord_errors() middleware covering error types, overrides, propagation, and success behavior.
  • Update server_stats tests to exercise the new mutate-based configuration flow while preserving behavior.

…lies

A central, type-aware complement to catch_errors(): catches discord.Forbidden,
NotFound, and HTTPException and sends localized ephemeral messages, so plugins
need not repeat the same try/except per command. Register after catch_errors()
so it handles Discord errors first and catch_errors() stays the outer fallback;
non-Discord errors propagate untouched. Includes 6 tests.
Replace the manual GuildLockManager + load/save ceremony with store.mutate,
consolidating on the single canonical lock domain the store already provides
(behavior-preserving). Removes the now-unused _locks and updates the plugin's
tests to seed config the same way. Reference migration for the other plugins
that still use the manual lock+load+save pattern.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 11, 2026
@sourcery-ai

sourcery-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new discord_errors() middleware for centralized, type-aware handling of Discord API exceptions, and migrates the server_stats plugin (and its tests) from manual GuildLockManager + load/save patterns to ServerConfigStore.mutate-based config updates, removing the custom lock manager.

Sequence diagram for discord_errors() middleware handling Discord API exceptions

sequenceDiagram
    participant Bot
    participant discord_errors_middleware as discord_errors
    participant CommandHandler as proceed
    participant Context as ctx

    Bot->>discord_errors_middleware: handler(ctx, proceed)
    discord_errors_middleware->>CommandHandler: await proceed()
    alt discord.Forbidden raised
        CommandHandler-->>discord_errors_middleware: discord.Forbidden
        discord_errors_middleware->>Context: respond(forbidden or ctx.t("errors.forbidden"), ephemeral=True)
    else discord.NotFound raised
        CommandHandler-->>discord_errors_middleware: discord.NotFound
        discord_errors_middleware->>Context: respond(not_found or ctx.t("errors.not_found"), ephemeral=True)
    else discord.HTTPException raised
        CommandHandler-->>discord_errors_middleware: discord.HTTPException
        discord_errors_middleware->>Context: respond(http_error or ctx.t("errors.http"), ephemeral=True)
    else no Discord error
        CommandHandler-->>discord_errors_middleware: success
    end
Loading

File-Level Changes

Change Details Files
Introduce discord_errors() middleware to catch common Discord API exceptions and respond with localized ephemeral messages.
  • Import discord in middleware module to access exception types.
  • Implement discord_errors() factory that logs and handles Forbidden, NotFound, and HTTPException with specific messages.
  • Ensure non-Discord exceptions are re-raised so outer handlers like catch_errors() remain effective.
  • Add tests that verify per-exception messaging, custom overrides, propagation of non-Discord errors, and success-path silence.
easycord/middleware.py
tests/test_discord_errors_middleware.py
Refactor server_stats plugin configuration writes to use ServerConfigStore.mutate instead of manual locking and load/save.
  • Remove GuildLockManager usage and the _locks attribute from ServerStatsPlugin, relying on the store’s own locking domain.
  • Introduce small helper callbacks that accept ServerConfig and perform stats setup/teardown mutations inside mutate.
  • Replace explicit lock/load/set_other/save sequences with single mutate calls for setup and teardown paths.
  • Update server_stats tests to use store.mutate for setting and clearing config, preserving behavior around guild isolation and teardown flows.
easycord/plugins/server_stats.py
tests/test_server_stats.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@rolling-codes, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e49cab83-ed50-47a9-ab5e-10dc548c6273

📥 Commits

Reviewing files that changed from the base of the PR and between 931f369 and 2063e92.

📒 Files selected for processing (4)
  • easycord/middleware.py
  • easycord/plugins/server_stats.py
  • tests/test_discord_errors_middleware.py
  • tests/test_server_stats.py

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add discord_errors middleware; migrate server_stats to store.mutate

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add discord_errors() middleware to convert Discord API exceptions into localized ephemeral
 replies.
• Migrate server_stats config writes to ServerConfigStore.mutate, removing redundant guild
 locking.
• Add/adjust tests to cover middleware behavior and updated config mutation flow.
Diagram

graph TD
  BOT["Bot"] --> CATCH["catch_errors()"] --> DERR["discord_errors()"] --> CMD["Slash command"] --> SSTATS["server_stats"] --> STORE["ServerConfigStore"] --> CFG[("Config JSON")]
  SSTATS --> DISCORD{{"Discord API"}}
  DISCORD -. "API errors" .-> DERR

  subgraph Legend
    direction LR
    _svc["Component"] ~~~ _db[("Data store")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Per-command error handling via @command_error
  • ➕ Highly tailored messages per command and per failure mode
  • ➕ Keeps global middleware stack simpler
  • ➖ Duplicates common try/except blocks across plugins
  • ➖ Easy to miss coverage in new commands; inconsistent UX across plugins
2. Execution-wrapping decorator (e.g., @safe_discord)
  • ➕ Explicit opt-in on sensitive commands
  • ➕ Can encode custom per-command messaging
  • ➖ Risks interfering with command signature/introspection if not carefully implemented
  • ➖ Still requires broad adoption across plugins to get consistent behavior
3. Single global catch-all including Discord exceptions
  • ➕ One place to handle everything; simplest registration story
  • ➖ Harder to keep non-Discord exceptions propagating to existing fallbacks as intended
  • ➖ Less type-specific messaging unless additional branching is added anyway

Recommendation: Keep the middleware approach: it fits the framework’s execution model, centralizes type-specific Discord API handling without per-command boilerplate, and preserves the existing separation where non-Discord exceptions continue to flow to outer fallbacks (e.g., catch_errors). The store.mutate migration is also the right direction because it consolidates on the store’s canonical per-guild lock and avoids cross-lock reasoning.

Files changed (4) +190 / -37

Enhancement (1) +72 / -0
middleware.pyAdd discord_errors() middleware for Discord API exception replies +72/-0

Add discord_errors() middleware for Discord API exception replies

• Introduces a new middleware that catches discord.Forbidden, discord.NotFound, and discord.HTTPException during command execution. Sends localized, type-specific ephemeral responses (with optional overrides) while letting non-Discord exceptions propagate to outer handlers.

easycord/middleware.py

Refactor (1) +8 / -9
server_stats.pyUse ServerConfigStore.mutate for atomic server_stats config writes +8/-9

Use ServerConfigStore.mutate for atomic server_stats config writes

• Replaces the plugin’s explicit GuildLockManager + load/save sequence with ServerConfigStore.mutate callbacks for setup/teardown writes. Removes the plugin-local lock state, consolidating all write serialization into the store’s per-guild lock.

easycord/plugins/server_stats.py

Tests (2) +110 / -28
test_discord_errors_middleware.pyAdd coverage for discord_errors middleware behavior +86/-0

Add coverage for discord_errors middleware behavior

• Adds tests for each handled Discord exception type, custom message overrides, non-Discord exception propagation, and the success path producing no response.

tests/test_discord_errors_middleware.py

test_server_stats.pyUpdate server_stats tests to seed config via store.mutate +24/-28

Update server_stats tests to seed config via store.mutate

• Refactors test setup and assertions to configure server_stats state using ServerConfigStore.mutate rather than plugin-local locks and explicit load/save calls. Keeps existing behavioral coverage while aligning with the new write path.

tests/test_server_stats.py

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In discord_errors, consider whether logger.warning for Forbidden/NotFound and logger.error for all HTTPExceptions is the right granularity for your logs, as these may be routine occurrences and could create noisy logs over time.
  • The three exception branches in discord_errors repeat the same ctx.respond pattern with only message text and log level differing; factoring this into a small helper (e.g., taking the message, log method, and error label) would reduce duplication and make future changes less error-prone.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `discord_errors`, consider whether `logger.warning` for `Forbidden`/`NotFound` and `logger.error` for all `HTTPException`s is the right granularity for your logs, as these may be routine occurrences and could create noisy logs over time.
- The three exception branches in `discord_errors` repeat the same `ctx.respond` pattern with only message text and log level differing; factoring this into a small helper (e.g., taking the message, log method, and error label) would reduce duplication and make future changes less error-prone.

## Individual Comments

### Comment 1
<location path="easycord/middleware.py" line_range="387-390" />
<code_context>
+                    ),
+                    ephemeral=True,
+                )
+        except discord.HTTPException as exc:
+            logger.error("HTTPException in %s: %s", ctx.command_name, exc)
+            with contextlib.suppress(Exception):
+                await ctx.respond(
</code_context>
<issue_to_address>
**suggestion:** Use `logger.exception` to retain the traceback for HTTP errors.

This branch only logs the exception message, so the traceback is lost and debugging HTTP issues becomes harder. Using `logger.exception("HTTPException in %s", ctx.command_name)` will capture the full stack trace while keeping the log concise and independent of `exc.__str__`.

```suggestion
        except discord.HTTPException as exc:
            logger.exception("HTTPException in %s", ctx.command_name)
            with contextlib.suppress(Exception):
                await ctx.respond(
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread easycord/middleware.py
@rolling-codes
rolling-codes merged commit 88373bb into main Aug 11, 2026
16 checks passed
@rolling-codes
rolling-codes deleted the feat/plugin-ergonomics branch August 11, 2026 20:30
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Lost Discord stack traces 🐞 Bug ◔ Observability
Description
discord_errors() consumes Discord exceptions so catch_errors() never logs the traceback, but the
new log lines don’t include exc_info, losing the call site and stack trace for production
debugging. This is especially impactful for discord.Forbidden/NotFound where only a generic
warning is emitted.
Code

easycord/middleware.py[R365-368]

+        except discord.Forbidden:
+            logger.warning("Forbidden in %s", ctx.command_name)
+            with contextlib.suppress(Exception):
+                await ctx.respond(
Relevance

●●● Strong

Team accepts adding logging for diagnosability; adding exc_info/exception is a low-risk
observability win.

PR-#86

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
discord_errors() swallows these exceptions and only logs a message string, while catch_errors()
(which previously would have handled these) logs full tracebacks via logger.exception. Because the
exception is consumed, the traceback is no longer emitted anywhere.

easycord/middleware.py[311-329]
easycord/middleware.py[332-399]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`discord_errors()` catches `discord.Forbidden`, `discord.NotFound`, and `discord.HTTPException` and does not re-raise them, which is intended, but it logs without `exc_info`. Since these exceptions no longer reach `catch_errors()` (which uses `logger.exception`), the traceback is lost.

## Issue Context
- `catch_errors()` logs full tracebacks with `logger.exception`, but only for exceptions that bubble out.
- `discord_errors()` intercepts Discord exceptions earlier in the chain, so it becomes the primary logging point for these failures.

## Fix Focus Areas
- easycord/middleware.py[311-399]

## Suggested change
- Capture the exception (`except discord.Forbidden as exc:` / `except discord.NotFound as exc:` / `except discord.HTTPException as exc:`) and log with traceback, e.g.:
 - `logger.warning("Forbidden in %s", ctx.command_name, exc_info=exc)`
 - `logger.warning("NotFound in %s", ctx.command_name, exc_info=exc)`
 - `logger.error("HTTPException in %s", ctx.command_name, exc_info=exc)`
 - or use `logger.exception(...)` where appropriate if you want stack traces consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. discord_errors not in public API 📘 Rule violation ⌂ Architecture
Description
The new test imports discord_errors from easycord.middleware, but the public API contract
requires consumers outside easycord/ to import only from the top-level easycord package. This
makes the test (and any external consumers copying it) rely on a non-public module path.
Code

tests/test_discord_errors_middleware.py[9]

+from easycord.middleware import discord_errors
Relevance

●● Moderate

Public-API import expectations unclear; they do curate exports, but no clear precedent for this
exact module import.

PR-#62

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 526296 restricts external imports to the public top-level easycord API. The added
test imports from easycord.middleware, and easycord/__init__.py only re-exports AnalyticsStore
and analytics_middleware from that module (not discord_errors).

Rule 526296: Only import public easycord API from outside the package
tests/test_discord_errors_middleware.py[9-9]
easycord/init.py[52-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Code outside the `easycord/` package should not import from `easycord.*` submodules unless the symbol is part of the public, top-level API. The new test imports `discord_errors` from `easycord.middleware`, but `discord_errors` is not re-exported from `easycord/__init__.py`.

## Issue Context
This PR adds a new public-facing middleware. Tests serve as examples for users, so they should use only the supported public import surface.

## Fix Focus Areas
- tests/test_discord_errors_middleware.py[9-9]
- easycord/__init__.py[52-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Ordering guidance too narrow 🐞 Bug ⚙ Maintainability
Description
The discord_errors() docstring says to register it after catch_errors() so it runs closer to the
command, but middleware execution depends on append order and the stack can include other built-ins
(e.g., rate_limit). The guidance can lead to placing discord_errors() outside later middleware
unintentionally, expanding its scope beyond “while running a command.”
Code

easycord/middleware.py[R347-350]

+    Register it *after* :func:`catch_errors` so it runs closer to the command
+    and handles Discord errors first, leaving ``catch_errors`` as the outer
+    fallback for everything else::
+
Relevance

●● Moderate

Doc guidance nuance is subjective; team sometimes rejects doc-related tweaks unless necessary.

PR-#106

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Bot.use() appends middleware and build_chain() wraps in reverse, so append order determines what
discord_errors() wraps. The doc snippet only mentions catch_errors(), while the default security
baseline also includes other middleware like rate_limit().

easycord/_bot_events.py[70-89]
easycord/middleware.py[31-40]
easycord/managers.py[29-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The docstring guidance for registering `discord_errors()` only references `catch_errors()`, but in practice middleware order is determined by append order + `build_chain(reversed(...))`. Users may have other middleware (e.g., `rate_limit`) and could place `discord_errors()` in a position that changes what it wraps/catches.

## Issue Context
- Middlewares are appended via `Bot.use()`.
- The first middleware in the list runs first (outermost), because the chain is built by iterating `reversed(middleware)`.

## Fix Focus Areas
- easycord/middleware.py[31-40]
- easycord/middleware.py[338-353]

## Suggested change
Update the `discord_errors()` docstring to explicitly describe ordering in terms of “outermost vs innermost”, e.g.:
- Recommend registering `catch_errors()` early (outermost) and registering `discord_errors()` late (innermost/closest to the command), ideally after other middleware that should remain outside it (like rate limiting / auth gates).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Hardcoded defaults in discord_errors 📘 Rule violation ⚙ Maintainability
Description
The new discord_errors() middleware sends user-facing English fallback strings via default=...,
which bypasses the localization-resource requirement. This can lead to inconsistent/non-localized UX
when translations are expected.
Code

easycord/middleware.py[R370-373]

+                    or ctx.t(
+                        "errors.forbidden",
+                        default="I don't have permission to do that.",
+                    ),
Relevance

● Weak

Prior localization-enforcement suggestions for hardcoded user strings were rejected; likely okay to
keep English defaults.

PR-#123
PR-#63

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 526292 requires user-facing text to come from localization keys/resources, not
hardcoded phrases. The added middleware uses hardcoded English sentences as default= fallbacks for
errors.forbidden, errors.not_found, and errors.http.

Rule 526292: Use localization keys for all plugin user-facing text
easycord/middleware.py[365-395]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`discord_errors()` embeds user-facing fallback strings directly in code via `ctx.t(..., default="...")`, which violates the requirement to use localization keys/resources for user-facing text.

## Issue Context
This middleware is intended to produce localized ephemeral error messages. To keep strings governed by i18n resources, the English defaults should live in the localization catalogs (or a built-in catalog) rather than inline in the response path.

## Fix Focus Areas
- easycord/middleware.py[365-397]
- easycord/bot.py[133-141]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 41 rules

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

import discord
import pytest

from easycord.middleware import discord_errors

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. discord_errors not in public api 📘 Rule violation ⌂ Architecture

The new test imports discord_errors from easycord.middleware, but the public API contract
requires consumers outside easycord/ to import only from the top-level easycord package. This
makes the test (and any external consumers copying it) rely on a non-public module path.
Agent Prompt
## Issue description
Code outside the `easycord/` package should not import from `easycord.*` submodules unless the symbol is part of the public, top-level API. The new test imports `discord_errors` from `easycord.middleware`, but `discord_errors` is not re-exported from `easycord/__init__.py`.

## Issue Context
This PR adds a new public-facing middleware. Tests serve as examples for users, so they should use only the supported public import surface.

## Fix Focus Areas
- tests/test_discord_errors_middleware.py[9-9]
- easycord/__init__.py[52-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread easycord/middleware.py
Comment on lines +365 to +368
except discord.Forbidden:
logger.warning("Forbidden in %s", ctx.command_name)
with contextlib.suppress(Exception):
await ctx.respond(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Lost discord stack traces 🐞 Bug ◔ Observability

discord_errors() consumes Discord exceptions so catch_errors() never logs the traceback, but the
new log lines don’t include exc_info, losing the call site and stack trace for production
debugging. This is especially impactful for discord.Forbidden/NotFound where only a generic
warning is emitted.
Agent Prompt
## Issue description
`discord_errors()` catches `discord.Forbidden`, `discord.NotFound`, and `discord.HTTPException` and does not re-raise them, which is intended, but it logs without `exc_info`. Since these exceptions no longer reach `catch_errors()` (which uses `logger.exception`), the traceback is lost.

## Issue Context
- `catch_errors()` logs full tracebacks with `logger.exception`, but only for exceptions that bubble out.
- `discord_errors()` intercepts Discord exceptions earlier in the chain, so it becomes the primary logging point for these failures.

## Fix Focus Areas
- easycord/middleware.py[311-399]

## Suggested change
- Capture the exception (`except discord.Forbidden as exc:` / `except discord.NotFound as exc:` / `except discord.HTTPException as exc:`) and log with traceback, e.g.:
  - `logger.warning("Forbidden in %s", ctx.command_name, exc_info=exc)`
  - `logger.warning("NotFound in %s", ctx.command_name, exc_info=exc)`
  - `logger.error("HTTPException in %s", ctx.command_name, exc_info=exc)`
  - or use `logger.exception(...)` where appropriate if you want stack traces consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread easycord/middleware.py
Comment on lines +347 to +350
Register it *after* :func:`catch_errors` so it runs closer to the command
and handles Discord errors first, leaving ``catch_errors`` as the outer
fallback for everything else::

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

4. Ordering guidance too narrow 🐞 Bug ⚙ Maintainability

The discord_errors() docstring says to register it after catch_errors() so it runs closer to the
command, but middleware execution depends on append order and the stack can include other built-ins
(e.g., rate_limit). The guidance can lead to placing discord_errors() outside later middleware
unintentionally, expanding its scope beyond “while running a command.”
Agent Prompt
## Issue description
The docstring guidance for registering `discord_errors()` only references `catch_errors()`, but in practice middleware order is determined by append order + `build_chain(reversed(...))`. Users may have other middleware (e.g., `rate_limit`) and could place `discord_errors()` in a position that changes what it wraps/catches.

## Issue Context
- Middlewares are appended via `Bot.use()`.
- The first middleware in the list runs first (outermost), because the chain is built by iterating `reversed(middleware)`.

## Fix Focus Areas
- easycord/middleware.py[31-40]
- easycord/middleware.py[338-353]

## Suggested change
Update the `discord_errors()` docstring to explicitly describe ordering in terms of “outermost vs innermost”, e.g.:
- Recommend registering `catch_errors()` early (outermost) and registering `discord_errors()` late (innermost/closest to the command), ideally after other middleware that should remain outside it (like rate limiting / auth gates).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@rolling-codes rolling-codes mentioned this pull request Aug 11, 2026
5 tasks
rolling-codes added a commit that referenced this pull request Aug 11, 2026
Release **v5.61.0**, bundling everything merged since v5.60.0.

## Included
- **#128** — offline `easycord try` CLI (run a slash command without
Discord)
- **#129** — plugin ergonomics helpers: `PluginConfigHelper` (atomic
config via `store.mutate`), decorator stacks (`slash_admin_command`
etc.), `TaskManager`/`TimerManager`
- **#130** — `discord_errors()` middleware + `server_stats`
`store.mutate` migration
- **#131** — `slash_mod_command` now uses `moderate_members` (real perm)
instead of the nonexistent `timeout_members`

## Version bump
`5.60.0 → 5.61.0` (minor: additive features + fixes, no breaking
changes) across `pyproject.toml`, `easycord/__init__.py`, `README.md`,
`docs/getting-started.md`; CHANGELOG entry added.

## Gates (per RELEASE.md)
- [x] `check_release_metadata.py` — no drift
- [x] `ruff check ... --select E9,F63,F7,F82` — clean
- [x] `verify_plugin_tests.py` — thresholds met
- [x] `pytest` — 1865 passed
- [ ] `pyright easycord tests` — 12 errors / 135 warnings, all
**pre-existing on main** and unrelated to this release (CI does not gate
on pyright); noted, not introduced here.

After merge: tag `v5.61.0` and publish a GitHub Release with the built
wheel + sdist. PyPI `twine upload` left as a manual step (requires
credentials).

## Summary by Sourcery

Release EasyCord v5.61.0 with new CLI tooling, plugin ergonomics
helpers, Discord error handling middleware, and minor fixes, plus
corresponding version and metadata updates.

New Features:
- Add offline `easycord try` CLI to run registered slash commands
without Discord connectivity.
- Introduce plugin ergonomics helpers including config mixin, decorator
stacks, and task/timer lifecycle managers.
- Add `discord_errors()` middleware to handle common Discord API errors
with user-facing messages.

Bug Fixes:
- Correct `slash_mod_command` to require the real `moderate_members`
permission instead of nonexistent `timeout_members`.
- Fix `TaskManager.track` race that could leave replacement tasks
untracked and uncancellable.
- Remove unused imports in decorator stacks flagged by static analysis.

Enhancements:
- Route `server_stats` configuration writes through
`ServerConfigStore.mutate` for consolidated locking and consistency.

Build:
- Bump project version to 5.61.0 in pyproject, package metadata, and
module `__version__`, and add corresponding changelog entry.
- Update release metadata entries for wheel and source distribution
artifacts.

Documentation:
- Update README and getting-started docs to reference the v5.61.0
release, download URLs, and badges.

Co-authored-by: tee <Thomas_BIRRELL@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants