Skip to content

fix: answer Discord button presses delivered over the Gateway - #557

Merged
fclairamb merged 2 commits into
mainfrom
fix/issue-550-discord-gateway-interactions
Oct 14, 2026
Merged

fclairamb merged 2 commits into
mainfrom
fix/issue-550-discord-gateway-interactions

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Closes #550

Report

When the Discord bot posts an incident there are three buttons [...] clicking any of these buttons produces an error in Discord saying "The bot did not respond quick enough." Checking the logs for SolidPing, it doesn't appear to receive any command when clicking the buttons.

Cause

When a Discord application has no Interactions Endpoint URL in the Developer Portal, Discord sends button presses and slash commands over the Gateway as INTERACTION_CREATE instead of over HTTPS. GatewaySupervisor.handleDispatch (server/internal/integrations/discord/gateway.go) only handled READY, RESUMED, MESSAGE_CREATE and GUILD_DELETE, so the press was dropped as an unhandled event at debug level. Nothing showed in the logs and Discord timed out.

Fix

  • The Gateway handles INTERACTION_CREATE by running it through the same DispatchInteraction as the HTTPS endpoint, then answering with POST /interactions/{id}/{token}/callback (BotClient.CreateInteractionResponse).
  • The interaction token is removed from callback errors before they are logged.
  • Docs (configuration/notifications.md) and wiki/discord/README.md now say the endpoint URL is optional when the Gateway is on.

Test plan

  • TestGateway_AnswersInteraction: pushes an INTERACTION_CREATE and asserts the callback path, type and ephemeral reply. Fails without the fix, passes with it.
  • go test -race ./internal/integrations/discord/... passes. golangci-lint on the package is clean.
  • Live test with a new Discord app, the fix build running locally behind a public tunnel:
    • No endpoint URL (Gateway): Acknowledge, I'm unavailable and Escalate all answer. Acknowledge rewrites the message, removes the buttons, posts in the thread and sets acknowledgedAt.
    • Endpoint URL set (HTTPS): Discord's signed probes pass on save, and the same three buttons answer through POST /api/v1/integrations/discord/interactions.

🤖 Generated with Claude Code

Clicking Acknowledge, Not available or Escalate showed "the application did
not respond" and SolidPing logged nothing. A Discord app with no Interactions
Endpoint URL gets its interactions as Gateway INTERACTION_CREATE events, and
handleDispatch (gateway.go:497) dropped them as unhandled. The Gateway now runs
them through the same DispatchInteraction and answers with the interaction
callback. TestGateway_AnswersInteraction fails without the fix.

Closes #550

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fclairamb fclairamb mentioned this pull request Oct 14, 2026
@fclairamb
fclairamb enabled auto-merge (squash) October 14, 2026 10:16
@codecov

codecov Bot commented Oct 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 46.15385% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.07%. Comparing base (9276c9b) to head (2bdcd5d).

Files with missing lines Patch % Lines
.../internal/integrations/discord/gateway_messages.go 40.00% 12 Missing ⚠️
server/internal/integrations/discord/bot_client.go 60.00% 2 Missing ⚠️

❌ Your patch check has failed because the patch coverage (46.15%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #557      +/-   ##
==========================================
- Coverage   83.08%   83.07%   -0.02%     
==========================================
  Files        1200     1200              
  Lines      123466   123492      +26     
==========================================
  Hits       102587   102587              
- Misses      20871    20897      +26     
  Partials        8        8              
Files with missing lines Coverage Δ
server/internal/integrations/discord/gateway.go 71.77% <100.00%> (+0.13%) ⬆️
server/internal/integrations/discord/bot_client.go 91.66% <60.00%> (-1.25%) ⬇️
.../internal/integrations/discord/gateway_messages.go 68.85% <40.00%> (-5.66%) ⬇️

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fclairamb
fclairamb merged commit 3b5937f into main Oct 14, 2026
25 of 26 checks passed
@fclairamb
fclairamb deleted the fix/issue-550-discord-gateway-interactions branch October 14, 2026 10:37
fclairamb added a commit that referenced this pull request Oct 14, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.38.4](v0.38.3...v0.38.4)
(2026-10-14)


### Features

* **http:** **HTTP results split the response time into DNS, connect,
TLS, server and download time.** The result page shows a Timing card,
and the check page has a Timing chart next to the response time. Each
probe now opens a new connection, so a check that runs more often than
every 90 s now counts connect and TLS time in its response time. The new
metrics are `dns_time_ms`, `connect_time_ms`, `tls_time_ms`,
`ttfb_time_ms`, `download_time_ms` and `redirect_time_ms`.
([9947d0b](9947d0b))
* **http:** **HTTP checks can test response headers with equals,
contains, absent and other operators.** The new `headerAssertions` takes
the same operators as body assertions, plus `exists` and `not_exists`.
Rules like "`X-Powered-By` must not be sent" or "`Cache-Control` must
not contain `no-store`" are now possible. The check form has a new
"Header assertions" section, and `headers_pattern` keeps working.
([9947d0b](9947d0b))
* **http:** **An HTTP check can test where its redirects end up.** Set
`expectedFinalUrl`, and the check goes down when the final URL is
different, for example a login or parking page. The comparison ignores
the case of the scheme and host, default ports and the fragment. The
failure output includes `final_url`.
([9947d0b](9947d0b))
* **dash0:** **An HTTP check can be copied as a cURL command.** "Copy as
cURL" is in the check page menu and on the incident page. The command
has the method, headers, body, redirects, TLS verification, HTTP version
and timeout. Secrets are never included: basic auth becomes
`$SOLIDPING_BASIC_AUTH`, and a comment line says when secret headers
were left out.
([9947d0b](9947d0b))
* **checks:** **Checks can send a new value on every run to get past a
CDN cache.** `${var:timestamp}`, `${var:iso8601}`, `${var:uuid}` and
`${var:random}` work anywhere `${env:...}` does, for example
`?cb=${var:timestamp}`. Within one run, each name keeps the same value.
The URL stored in a result now keeps `${param:...}` references instead
of the secret values.
([9947d0b](9947d0b))
* **icmp:** **An ICMP check can go down on packet loss or a slow round
trip.** Set `max_packet_loss_percent` or `max_avg_rtt_ms`. Before, one
reply out of five kept the check up. The incident gives the reason, for
example `packet loss 40% > 20%`.
([9947d0b](9947d0b))
* **notifications:** **Incident notifications say which regions see the
failure.** Alerts include "Failing from: Europe West, US East (2 of 3
regions)", and resolved alerts include "Failed from: ...". The webhook
payload has a new `incident.regions` with `failingRegions` (slugs),
`regionCount` and `failQuorum`. It is recorded when the incident opens
and updated when it escalates.
([9947d0b](9947d0b))
* **incidents:** **Open incidents that nobody has acknowledged can send
reminders.** Set an interval from 15 min to 24 h on the check
(`reminderIntervalSeconds`) or as an organization default. The check's
channels then get `incident.reminder`: "Still down after 1 h 15 min, not
acknowledged (reminder 3)". Reminders are off by default and stop on
ack, snooze or resolve. Twilio and PagerDuty channels never get them.
([9947d0b](9947d0b))
* **mcp:** **The MCP server lists 23 tools by default instead of 46.**
Tools are grouped in toolsets. `core` and `statuspages` are listed by
default, and `admin` (integrations and maintenance windows) must be
requested. Add `?toolsets=all` to the endpoint URL to list them all, or
use `SP_MCP_TOOLSETS` or `--toolsets` with stdio. The 19 replaced tool
names still work and are listed with `toolsets=legacy`.
([9947d0b](9947d0b))
* **helm:** **The Helm chart can run SolidPing on PostgreSQL through
CloudNativePG.** Set `database.cnpg.enabled=true` to create a CNPG
`Cluster` and read the connection string from its secret. The cluster
survives `helm uninstall`. SQLite stays the default, so existing
installs are not moved on upgrade. The Kubernetes install page now
documents the chart.
([#544](#544))
([30218d5](30218d5))


### Bug Fixes

* **discord:** **Discord buttons work when the bot has no Interactions
Endpoint URL.** Acknowledge, I'm unavailable and Escalate failed with
"The bot did not respond quick enough", and nothing showed in the logs.
Discord sends the button press over the Gateway in that case, and
SolidPing ignored it. It now answers it like the HTTPS endpoint does, so
the endpoint URL is optional when the Gateway is on.
([#557](#557))
([3b5937f](3b5937f))
* **http:** **An HTTP check with no expected status passes on any 2xx,
not only 200.** Healthy endpoints answering 201, 202 or 204 showed as
down. Existing checks change too. To keep the old behaviour, set
`expectedStatusCodes: ["200"]`.
([9947d0b](9947d0b))
* **db:** **A first start that was interrupted no longer leaves a
database that never starts again.** Stopping a container during its
first start could end with `no such column:
organization.default_escalation_policy_uid` on every boot. A failed or
interrupted migration now stays pending and runs again. On SQLite, the
first boot builds the database in a temporary file and renames it when
done, and each upgrade first saves a backup
(`solidping.db.pre-<migration>.bak`) when there is room on disk.
([#549](#549))
([c7479ed](c7479ed))
* **webpush:** **Push notifications keep working after the first
restart.** On a fresh install, the push key served during the first run
was never stored, so the first restart made a new one and every browser
subscribed in between stopped receiving pushes. The key is now stored
before it is used. A browser hit by this can turn push off and on again.
([#539](#539))
([caf64b4](caf64b4))
* **server:** **HEAD requests get the same answer as GET.** Every route
answered `405 Method Not Allowed` to HEAD, so external uptime monitors
that probe with HEAD saw SolidPing status pages as down.
([9947d0b](9947d0b))
* **checks:** **Run now during a run in progress now gives a result.**
The request was dropped and the region reported `running`, so with a 1 h
period the dashboard waited forever. The region now answers `queued` and
runs again as soon as the current run ends. Editing a check while it
runs no longer loses that run's result.
([9947d0b](9947d0b))
* **server:** **A fresh instance idles at about 30 MiB of its own memory
instead of 140.** The seeded admin password was hashed with 64 MiB
argon2id, and that memory stayed resident for minutes. Every login did
the same. Measured on linux/arm64, RssAnon went from 141 to 31 MiB 60 s
after first boot, and from 207 to 26 MiB 10 s after a login. The browser
sample checks are now created disabled, and a Linux host with 2 GiB of
RAM or less and no cgroup limit gets half its RAM as the soft memory cap
(`SP_RUNTIME_AUTO_MEMORY_LIMIT=false` turns this off).
([9947d0b](9947d0b))
* **auth:** **On a host with 2 GiB of memory or less, password hashing
uses the OWASP minimum argon2id profile.** A login now allocates 19 MiB
instead of 64 MiB (`m=19456 t=2 p=1`). This applies only to
single-process installs (`SP_NODE_ROLE=all`) where no argon2 cost is
set. Existing hashes are converted at the next login.
`SP_AUTH_PASSWORD_ARGON2_AUTO_PROFILE=false` keeps 64 MiB.
([9947d0b](9947d0b))
* **onboarding:** **A fresh install seeds 11 sample checks that work,
instead of about 75.** Most samples targeted services nobody runs
locally (databases, message brokers, game servers, test FTP servers), so
about half of them were down and the first dashboard was red. The 11
seeded checks are HTTP, DNS, SSL and TCP checks against public services.
Every sample is still in the create-check picker, and test and demo
modes seed them all.
([#537](#537))
([262c2cc](262c2cc))
* **logs:** **Logs pick a readable format on their own.** The new
default format, `auto`, gives JSON on Kubernetes, colored output in a
terminal or in a container, and plain text otherwise.
`SP_LOG_FORMAT=json`, `text` or `pretty` still wins. The colored format
has a timestamp and keeps attributes in a stable order. `NO_COLOR` and
`FORCE_COLOR` are honored. `LOG_LEVEL` is no longer reset to info once
the configuration loads.
([#536](#536))
([4c13730](4c13730))
* **logs:** **A fresh server prints a few lines at startup and the URL
to open.** It used to print 541 lines in its first minute and never said
where to go. Check runs are DEBUG now. An INFO line is written when a
check goes down, comes back up, warns or goes stale, and names the
check. Workers and sample seeding print one line each.
([#538](#538))
([bbe682a](bbe682a))
* **logs:** **A fresh start no longer warns about normal things.** The
SQL errors on a missing table, the worker hostname, the retention
setting, Sentry and Redis warnings are gone or moved to DEBUG. The
metrics endpoint gets one line instead of two that contradicted each
other. A fresh start now prints one WARN: credentials encryption is
disabled. ([#539](#539))
([caf64b4](caf64b4))
* **auth:** **Signing in on the demo org no longer files a join
request.** A visitor who signed up from the live demo was told "A join
request was sent to its admins", and nobody ever answered it. Sign-ins
on the demo org, and on the `default` org in SaaS mode, now land on the
create-your-org screen. On self-hosted installs, sign-ins on the
`default` org still send join requests to its admins.
([9947d0b](9947d0b))
* **dash0:** **A first-time visitor is no longer told their session
expired.** Opening `/` or `/d/` in a fresh browser showed "Your session
has expired. Please log in again." The visitor now lands on the sign-in
page with no banner. An expired or revoked session still shows it.
([9947d0b](9947d0b))
* **onboarding:** **The first check is no longer pre-filled with your
mail provider.** The empty dashboard suggests your email domain as the
first URL to monitor, and skips free webmail. The skip list only had
`.com` names, so a `gmx.de`, `yahoo.fr` or `orange.fr` address got its
mail provider suggested. Country variants and national providers are now
skipped too. ([#532](#532))
([b3e6672](b3e6672))
* **webhooks:** **Webhook signing secrets work as is with the official
Standard Webhooks libraries.** New secrets are `whsec_` followed by
standard base64 with padding. The dashboard and API show older secrets
in the same form. It is the same key, so receivers that already verify
keep working and nothing needs rotating. A pasted secret that is not
valid base64 now gets a 400 on create and update.
([9947d0b](9947d0b))
* **config:** **`SP_AUDIT_RETENTION_DAYS=0` keeps audit events forever,
as documented.** The value 0 was ignored, and the cleanup still deleted
events older than 365 days. 0 now means "keep forever" in the env
variable, in `config.yml` and in the `audit.retention_days` server
parameter.
([9947d0b](9947d0b))
* **helm:** **Uploaded files survive a pod restart on PostgreSQL.** With
`database.type=postgres`, `/data` was not mounted, so logos, status page
assets and screenshots were lost on every restart. `/data` is now
mounted whatever the database.
([#544](#544))
([30218d5](30218d5))
* **dash0:** **Toasts no longer cover the Run now, Disable and Edit
buttons on a check page.** After you created a check, the "Not on any
status page yet" toast sat over these buttons. It is now a hint inside
the check page, with its "Publish it" action. All toasts now start below
the page header.
([9947d0b](9947d0b))
* **dash0:** **Region events name the region.** On the Events page,
`region.offline` and `region.recovered` had an empty detail. They now
show the region name.
([#552](#552))
([f3ac93e](f3ac93e))
* **dash0:** **Page titles stay readable on a phone.** On narrow
screens, the header buttons squeezed the title to one word per line, for
example on SLOs and Escalation policies. The buttons now wrap below the
title. ([#553](#553))
([01d1c95](01d1c95))
* **dash0:** **The status page editor works on a phone.** Section
headers and resource rows wrap instead of overflowing.
([#554](#554))
([fb53d50](fb53d50))
* **http:** **Forcing HTTP/2 against an HTTP/1.1-only server always
gives the clear error.** About 2% of such cleartext probes reported a
generic connection error instead of "expected HTTP/2, but the server did
not negotiate it".
([#547](#547))
([b74bf7c](b74bf7c))
* **server:** **The platform watchdog no longer flags daily checks as
stale.** A heartbeat check that runs once a day was reported as "silent
for 95h" between the compaction of its last result and its next run. The
watchdog now also reads the check's last result time.
([#542](#542))
([bd4af36](bd4af36))
* **api:** **The number of check types is the same everywhere.** The
docs said 39 in one place and 43 in another. Every page now says 43.
`GET /api/v1/check-types` has a new `excludedFromTally` field, true for
`sleep` and `private-location`. The incidents API docs also list the
parameters the server actually reads (`state`, `checkUid`, `cursor`).
([9947d0b](9947d0b))
* **deps:** update github.com/dop251/goja digest to 0c308c5
([#546](#546))
([cba413a](cba413a))
* **deps:** update github.com/dop251/goja digest to 245c54f
([#551](#551))
([90b9c25](90b9c25))
* **deps:** update github.com/dop251/goja digest to 480b366
([#545](#545))
([b86f196](b86f196))
* **deps:** update go dependencies (non-major)
([#543](#543))
([87eb548](87eb548))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord Bot Response

1 participant