Known Issues and Lessons Learned
On this page
Purpose
This page is the deploying-jurisdiction’s first-stop reference for surprises Canopy has hit during its own development. Each entry names the symptom, the root cause, and the resolution.
For developer-facing internal notes (cargo deny advisories, contributor toolchain quirks), see the Contributor toolchain notes section below.
For runtime incident response, see Security Operations & Runbooks. For backup/restore, see Database Backup & Restore Runbook.
Devstack
| Symptom | Root cause | Resolution |
|---|---|---|
Garage S3 crashes on startup |
Garage v2.2.0+ requires |
Set both fields (already configured in |
|
Orphan containers from a prior compose project name shadowing the current one. |
The xtask devstack guard now passes |
Port conflicts after a crashed devstack |
Previous run didn’t shut down cleanly; ports stay bound. |
|
|
|
|
Cold devstack start takes 10+ minutes |
Building the workspace from scratch (Alpine target, full dep graph). Cached builds are seconds. |
First-time setup is unavoidable. Subsequent restarts use the named-volume cache. Don’t |
Compose / env edits don’t take effect after |
Devstack compose + env changes are applied by the xtask devstack guard, not a bare |
Drive the devstack with |
canopy-persons comes up with |
Secrets were delivered only via per-call process-env injection. Any compose recreate that didn’t thread the decrypted secret env — notably the post- |
Fixed (#734): the |
Keycloak
| Symptom | Root cause | Resolution |
|---|---|---|
Test users can’t log in |
Password hashes in |
The seeded definitions.json already uses bcrypt. If adding new test users, hash with |
Password grant flow fails for seeded users |
|
Set |
JWT validation fails with "issuer mismatch" |
When running in Docker, the JWT’s |
Configure |
Repeated JWKS fetches when an unknown |
The |
30-second debounce on forced refreshes is built into |
Testing
| Symptom | Root cause | Resolution |
|---|---|---|
Integration tests skipped silently in CI |
|
In CI, set |
Pre-push validate takes minutes longer than expected |
|
|
|
Test publishes an event then queries canopy-security; RabbitMQ delivery timing can race the query. |
Re-run usually passes. Tracked as a known transient. If persistent, increase the polling interval in the test. |
Pre-push hook runs E2E unconditionally |
Playwright runs in a Docker container; no |
Container builds + runs in ~50 s with warm cache. To skip locally: |
First container test run is slow |
|
Subsequent runs hit the |
An E2E |
|
Grab |
Can’t reproduce a server-side error/outage state in a Playwright test |
The failing upstream call is server-to-server, so route-interception in the browser can’t trigger it — the service must actually be down. |
|
JDM Rulesets (zen-engine 0.55)
zen-engine has several non-obvious behaviours that bit Canopy during the JDM rewrite. Document them here so future ruleset authors don’t re-discover them.
| Symptom | Root cause | Resolution |
|---|---|---|
|
|
Use |
Decision-table cell with quoted-string condition fails to match |
zen-engine 0.55 string matching in DT cells is unreliable. |
Use an |
Ternary |
zen-engine’s expression evaluator chokes on string literals inside ternaries. |
Build string outputs in a separate expression node. Or move the branching into a DT. |
DT output cell typed as |
zen-engine emits all DT outputs as strings. |
Parse with |
Downstream nodes see |
Transform nodes drop input fields by default. |
Set |
Event Bus (RabbitMQ)
| Symptom | Root cause | Resolution |
|---|---|---|
Subscriber struct deserialisation fails on FTI fields |
ADR-004 publisher-side wire scrubbing drops FTI field names before publishing. |
Subscriber structs must mark possibly-scrubbed fields |
Field-name drift between publisher and subscriber goes uncaught |
Schema-less JSON envelope; no compile-time check. |
Cross-service integration tests are the backstop. When adding a wire field, pick the subscriber-side name first and use it on the wire. |
Service publishes events but subscribers don’t receive |
Connection dropped (e.g. |
|
Database
| Symptom | Root cause | Resolution |
|---|---|---|
|
17 services + integration tests against one Postgres instance exhaust the default 100-connection cap. |
|
Build fails with "DATABASE_URL not set" during |
sqlx compile-time query verification needs a live DB or cached metadata. |
Production builds set |
Want to roll back a migration |
Migrations are forward-only by convention. No |
Write a corrective migration (additive fix). For data corruption, restore from backup per the runbook. |
Warning about DATABASE_URL not matching service name |
Under |
Warning is intentional. Errors only on truly inconsistent configurations. |
Cross-program orchestration
| Symptom | Root cause | Resolution |
|---|---|---|
Program determinations bucket as |
Per-program signing keys must be loaded both by the program service (signing) and canopy-eligibility (verification). Both env vars or both files. |
|
canopy-medicaid signature verification fails post-DB-roundtrip |
Service signed |
canopy-snap normalises |
Decimal benefit_amount serialises differently in-memory vs from DB |
|
Call |
Orchestrator-dispatched Medicaid determinations scored the applicant at a hardcoded age 30 (and $0 resources) |
The orchestrator builds one generic |
Resolve the applicant’s age/disability from |
Orchestrator-dispatched income/expense amounts reached programs un-normalized (weekly/annual mislabeled as monthly) |
The orchestrator threaded raw canopy-persons |
Shared |
When to add an entry here
After spending more than 30 minutes debugging something that turns out to be a class of issue, write it up here so the next person doesn’t pay the same cost. Categories above are not exhaustive — add new ones as needed.
Contributor toolchain notes
Developer-facing toolchain quirks (migrated here from the retired agent-facing known-issues note when the contributor docs moved to this Antora page). Operational/runtime issues go in the sections above; these are build/test-harness notes for contributors.
Cargo / toolchain
-
Transitive typst advisories: 4 advisories are suppressed in
deny.toml— all transitive via typst (yaml-rust unmaintained, bincode unmaintained, paste unmaintained, rsa Marvin Attack). No upstream fix available; monitor typst releases. -
cargo deny subsumes cargo audit: only
cargo deny checkruns in CI andcargo xtask validate.cargo auditis redundant when deny is present (deny checks advisories + licenses + bans). -
Rust 2024 reserves the
genkeyword: do not usegenas a variable name. Clippy warns, but the error message is confusing. -
set_var/remove_varare unsafe in Rust 2024: you cannot unit-test environment-variable-dependent functions (e.g.is_dev_env()) underunsafe_code = "deny". Document the behavior with a comment instead.
Test harness config
-
nextest integration concurrency cap (
.config/nextest.toml): integration tests are capped (test-threads = 4) to avoid overwhelming the shared devstack with concurrent HTTP connections. This is a canopy-specific divergence from the universal nextest profile, recorded in.claude/sync-overrides.toml(cfg-nextest).
OpenAPI snapshots drift from doc comments
utoipa embeds the doc comments on [derive(ToSchema)] structs/fields and [utoipa::path] handlers into the generated OpenAPI description fields. So editing those doc comments changes the OpenAPI contract — even a purely cosmetic rustdoc-link cleanup (e.g. [`Foo`] → a path-qualified link or a plain code span) drifts the committed snapshots under docs/modules/ROOT/openapi/.json. After any doc-comment edit on a schema type or a #[utoipa::path] handler, run cargo xtask api-docs --update and commit the regenerated snapshot *in the same MR.
The cargo xtask api-docs gate is environment-sensitive: it can pass locally while the running devstack still serves the old (matching) spec, and only surface the drift on a later push once the devstack is rebuilt — so a green local push is not proof the snapshots are in sync with main’s source. (This bit MR !638, whose rustdoc-link sweep edited `ToSchema doc comments in canopy-applications / -security / -tanf without regenerating; corrected in the follow-up that documented this note.)