Skip to content

fix(web): /c/[slug] 500 → 404 on unknown tag#10

Merged
xiaolai merged 1 commit into
mainfrom
fix-c-slug-500
May 6, 2026
Merged

fix(web): /c/[slug] 500 → 404 on unknown tag#10
xiaolai merged 1 commit into
mainfrom
fix-c-slug-500

Conversation

@xiaolai

@xiaolai xiaolai commented May 6, 2026

Copy link
Copy Markdown
Owner

Route audit found one dead-end: /c/<anything> returned 500 (Pages Router /_error shell) instead of 404 when the slug wasn't in the tags table.

Other dynamic routes (/post/[id], /u/[username], /projects/[slug], /office/persona/[name], /office/decision/[id]) all 404 correctly. The difference: /c/[slug] is the only one with generateStaticParams. With tags empty (fresh deploy, no submissions yet → no auto-tags), it returns [] and Next.js 15's static optimizer marked the route as prerendered-with-zero-params. Unknown slugs missed both the static index and the App Router's dynamic→notFound() path.

export const dynamic = "force-dynamic" keeps generateStaticParams as a build-time hint for when tags exist, but guarantees unknown slugs render dynamically → notFound() → 404 every time.

Coverage from the same audit

  • 18 static reader routes: all 200
  • 7 dynamic route types tested: all 200 on valid IDs, 404 on unknown
  • 6 API endpoints: 200 (public), 401 (auth-required), 404 (unknown ID)
  • /admin → 307 → /admin/queue (intentional)
  • /mod → 307 → /admin/queue (intentional)
  • /appeal/ → 307 → /login when unauthenticated (intentional)

No other dead ends found.

Test plan

  • After merge, hit https://claudepot.com/c/anything and confirm 404 with App Router not-found.tsx ("That page slipped through the gravity well") rather than the Pages Router 500 shell.

Route audit found /c/<unknown-slug> returning 500 (Pages Router
/_error shell) instead of 404 (App Router not-found.tsx). Other
dynamic routes (/post/[id], /u/[username], /projects/[slug],
/office/persona/[name], /office/decision/[id]) all correctly 404
on unknown ids; /c/[slug] was the only one that 500ed.

Cause: /c/[slug] is the only dynamic route that defines
generateStaticParams. With the tags table empty (no submissions →
no auto-tags yet on this fresh deploy), generateStaticParams
returns []. Next.js 15's static-optimization heuristic then
treated the route as fully prerendered with zero params, so
unknown slugs missed both the static index and the App Router's
dynamic-render → notFound() path.

Fix: `export const dynamic = "force-dynamic"`. The page reads
auth() (cookies) anyway, so it's request-scoped — force-dynamic
just makes that explicit. generateStaticParams stays as a
build-time hint for when tags exist, but unknown slugs
deterministically go through dynamic render → notFound() → 404.

Verified all 18 static reader routes 200, all 7 dynamic routes
return 200 for valid IDs and 404 (not 500) for unknown ones,
all 6 API smoke checks return their expected status codes
(200 / 401 / 404).
@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
claudepot-com Error Error May 6, 2026 0:56am

Request Review

@xiaolai xiaolai merged commit d400383 into main May 6, 2026
7 of 8 checks passed
@xiaolai xiaolai deleted the fix-c-slug-500 branch May 6, 2026 12:57
xiaolai added a commit that referenced this pull request May 8, 2026
HIGH fixes:
- register_from_browser: rollback private blob + cleanup temp dir
  on store.insert() failure (#4)
- Orphan detection: roundtrip check prevents lossy unsanitize_path
  from causing false-positive deletions (#1, #10)
- --from-token reads from stdin when value is "-" to avoid shell
  history exposure (#9)

MEDIUM fixes:
- swap switch(): rollback CC credentials on set_active_cli failure (#11)
- register_account_from_profile: delete orphaned private blob on
  insert failure (#17)
- remove_account: reorder to clear pointers + remove DB row before
  irreversible file deletions (#18)
- desktop switch(): propagate DB update failures instead of ignoring (#22)
- account.rs: propagate DB permission-setting failures (#20)
- project.rs: post-move failures become warnings instead of errors
  when directory already moved (#16)
- account remove --json: emit structured JSON response (#28)
- doctor summary: count expired accounts as warnings (#26)

LOW fixes:
- doctor: surface store.list() failures via db_error field (#29)
- keychain status: distinguish "no credential" from "access error" (#30)
xiaolai added a commit that referenced this pull request May 8, 2026
Independent Codex audit (5-dim mini, threadId 019de662) flagged
2 High + 8 Medium + 1 Low across the templates feature surface.
Independent verification (threadId 019de792) confirmed 10 fixes
landed; #11 is deferred with rationale.

### High

#1 prerun probe ignored route auth — `probe_sync` in
`automations/prerun.rs` now attaches `Authorization: Bearer
<key>` (or `Basic <key>` when `auth_scheme` is set) from the
route's `GatewayConfig.api_key` when non-empty. LiteLLM /
Cloudflare AI Gateway endpoints behind auth no longer false-
fail with 401.

#2 sidecar swap in `templates_apply_pending` —
`commands_templates.rs` now rejects early when
`pending.automation_id != automation_id`. Without this, an
attacker (or a buggy caller) could pair automation A's pending-
changes.json with automation B's broader apply policy.

### Medium

#3 Ollama probe detection too broad — was `cfg.base_url.contains
("/api/")` which false-positives on `/api/v1` (OpenRouter,
LiteLLM). Now narrowed to `:11434` port OR explicit `/api/tags`
path.

#4 hardcoded `~/.claudepot` — both `templates_pending_changes`
and `templates_read_report` now use `claudepot_data_dir()` from
claudepot-core, honoring `CLAUDEPOT_DATA_DIR` overrides
correctly.

#5 `supported_platforms` was only enforced in `templates_list`
— `templates_install` now ALSO rejects when
`!bp.supports(HostPlatform::current())`. The contract is
symmetric: a direct IPC bypass of the gallery filter no longer
lets a macOS-only template instantiate on Linux/Windows.

#6 Windows ACL trusted `USERNAME` — `tighten_consent_acl` now
resolves the current user SID via `whoami /user /fo csv /nh`,
grants `*<SID>:F` in icacls (well-known SID notation), grants
SYSTEM via the literal `S-1-5-18` SID, and surfaces every
failure path through `tracing::warn` instead of swallowing
silently.

#7 cron test wrote `CLAUDE_OAUTH_TOKEN` to disk via
`automation.extra_env` (which `install_shim` renders into
`run.sh`). Token now lives in a 0600 sibling file
(`<tmp>/.oauth-token`); a 0700 wrapper script
(`<tmp>/claude-with-token.sh`) reads the file at fire time and
exec's the real claude. The shim sees only the wrapper path,
never the token.

#8 cron test scheduled `now + 1 minute`, which races the case
where `install_shim + scheduler.register` span past the next
minute boundary — launchd would defer to the same time
tomorrow and the test would hang. Now schedules `now + 2
minutes`; deadline extended to 180s.

#9 cron test passed on a `"result":` substring match, which is
too permissive — error rows mention `result` too. Now parses
`stdout.log` as a JSON event array, finds the terminal
`result` event, asserts `is_error == false`. Verified locally:
the cron run produced `is_error=false result_first_80=
CRON_TEMPLATE_OK`.

#10 `CronCleanupGuard` did not restore the previous
`CLAUDEPOT_DATA_DIR` — risk of polluting subsequent tests in
the same process. Now captures the prior value as
`Option<OsString>` at construction and restores it on Drop.
Also explicitly zero-fills + unlinks the token file so a
tempdir-removal race doesn't leave the token on disk.

### Low (deferred)

#11 validator rebuilds globset matcher per call. Apply is
interactive (~5-50 items × 1-3 globs each, user reviews + clicks
per run); not a hot loop. Caching would require an
`ApplyConfig`-level matcher cache and is not worth the
complexity. Documented in the audit-fix verification report.

### Verification

Independent Codex re-audit on a fresh thread:
| Status | Count |
|---|---:|
| FIXED | 10 |
| NOT FIXED | 0 |
| PARTIAL | 0 |
| REGRESSED | 0 |
| DEFERRED | 1 |

Local validation:
- `cargo test --workspace` — all green
- `cargo test -p claudepot-core --test templates_real_llm
   cron_schedule_fires -- --ignored` — passes with new
  is_error=false assertion (110s wall, ~$0.30)

Audit threadId: 019de662-808c-7b62-80b3-e85d373f92b5
Verify threadId: 019de792-3515-7533-aac0-0203a96388be
xiaolai added a commit that referenced this pull request May 8, 2026
fix(web): /c/[slug] 500 → 404 on unknown tag
xiaolai added a commit that referenced this pull request May 8, 2026
Independent Codex audit (5-dim mini, threadId 019de662) flagged
2 High + 8 Medium + 1 Low across the templates feature surface.
Independent verification (threadId 019de792) confirmed 10 fixes
landed; #11 is deferred with rationale.

### High

#1 prerun probe ignored route auth — `probe_sync` in
`automations/prerun.rs` now attaches `Authorization: Bearer
<key>` (or `Basic <key>` when `auth_scheme` is set) from the
route's `GatewayConfig.api_key` when non-empty. LiteLLM /
Cloudflare AI Gateway endpoints behind auth no longer false-
fail with 401.

#2 sidecar swap in `templates_apply_pending` —
`commands_templates.rs` now rejects early when
`pending.automation_id != automation_id`. Without this, an
attacker (or a buggy caller) could pair automation A's pending-
changes.json with automation B's broader apply policy.

### Medium

#3 Ollama probe detection too broad — was `cfg.base_url.contains
("/api/")` which false-positives on `/api/v1` (OpenRouter,
LiteLLM). Now narrowed to `:11434` port OR explicit `/api/tags`
path.

#4 hardcoded `~/.claudepot` — both `templates_pending_changes`
and `templates_read_report` now use `claudepot_data_dir()` from
claudepot-core, honoring `CLAUDEPOT_DATA_DIR` overrides
correctly.

#5 `supported_platforms` was only enforced in `templates_list`
— `templates_install` now ALSO rejects when
`!bp.supports(HostPlatform::current())`. The contract is
symmetric: a direct IPC bypass of the gallery filter no longer
lets a macOS-only template instantiate on Linux/Windows.

#6 Windows ACL trusted `USERNAME` — `tighten_consent_acl` now
resolves the current user SID via `whoami /user /fo csv /nh`,
grants `*<SID>:F` in icacls (well-known SID notation), grants
SYSTEM via the literal `S-1-5-18` SID, and surfaces every
failure path through `tracing::warn` instead of swallowing
silently.

#7 cron test wrote `CLAUDE_OAUTH_TOKEN` to disk via
`automation.extra_env` (which `install_shim` renders into
`run.sh`). Token now lives in a 0600 sibling file
(`<tmp>/.oauth-token`); a 0700 wrapper script
(`<tmp>/claude-with-token.sh`) reads the file at fire time and
exec's the real claude. The shim sees only the wrapper path,
never the token.

#8 cron test scheduled `now + 1 minute`, which races the case
where `install_shim + scheduler.register` span past the next
minute boundary — launchd would defer to the same time
tomorrow and the test would hang. Now schedules `now + 2
minutes`; deadline extended to 180s.

#9 cron test passed on a `"result":` substring match, which is
too permissive — error rows mention `result` too. Now parses
`stdout.log` as a JSON event array, finds the terminal
`result` event, asserts `is_error == false`. Verified locally:
the cron run produced `is_error=false result_first_80=
CRON_TEMPLATE_OK`.

#10 `CronCleanupGuard` did not restore the previous
`CLAUDEPOT_DATA_DIR` — risk of polluting subsequent tests in
the same process. Now captures the prior value as
`Option<OsString>` at construction and restores it on Drop.
Also explicitly zero-fills + unlinks the token file so a
tempdir-removal race doesn't leave the token on disk.

### Low (deferred)

#11 validator rebuilds globset matcher per call. Apply is
interactive (~5-50 items × 1-3 globs each, user reviews + clicks
per run); not a hot loop. Caching would require an
`ApplyConfig`-level matcher cache and is not worth the
complexity. Documented in the audit-fix verification report.

### Verification

Independent Codex re-audit on a fresh thread:
| Status | Count |
|---|---:|
| FIXED | 10 |
| NOT FIXED | 0 |
| PARTIAL | 0 |
| REGRESSED | 0 |
| DEFERRED | 1 |

Local validation:
- `cargo test --workspace` — all green
- `cargo test -p claudepot-core --test templates_real_llm
   cron_schedule_fires -- --ignored` — passes with new
  is_error=false assertion (110s wall, ~$0.30)

Audit threadId: 019de662-808c-7b62-80b3-e85d373f92b5
Verify threadId: 019de792-3515-7533-aac0-0203a96388be
xiaolai added a commit that referenced this pull request May 8, 2026
fix(web): /c/[slug] 500 → 404 on unknown tag
xiaolai added a commit that referenced this pull request May 8, 2026
HIGH fixes:
- register_from_browser: rollback private blob + cleanup temp dir
  on store.insert() failure (#4)
- Orphan detection: roundtrip check prevents lossy unsanitize_path
  from causing false-positive deletions (#1, #10)
- --from-token reads from stdin when value is "-" to avoid shell
  history exposure (#9)

MEDIUM fixes:
- swap switch(): rollback CC credentials on set_active_cli failure (#11)
- register_account_from_profile: delete orphaned private blob on
  insert failure (#17)
- remove_account: reorder to clear pointers + remove DB row before
  irreversible file deletions (#18)
- desktop switch(): propagate DB update failures instead of ignoring (#22)
- account.rs: propagate DB permission-setting failures (#20)
- project.rs: post-move failures become warnings instead of errors
  when directory already moved (#16)
- account remove --json: emit structured JSON response (#28)
- doctor summary: count expired accounts as warnings (#26)

LOW fixes:
- doctor: surface store.list() failures via db_error field (#29)
- keychain status: distinguish "no credential" from "access error" (#30)
xiaolai added a commit that referenced this pull request May 8, 2026
Independent Codex audit (5-dim mini, threadId 019de662) flagged
2 High + 8 Medium + 1 Low across the templates feature surface.
Independent verification (threadId 019de792) confirmed 10 fixes
landed; #11 is deferred with rationale.

### High

#1 prerun probe ignored route auth — `probe_sync` in
`automations/prerun.rs` now attaches `Authorization: Bearer
<key>` (or `Basic <key>` when `auth_scheme` is set) from the
route's `GatewayConfig.api_key` when non-empty. LiteLLM /
Cloudflare AI Gateway endpoints behind auth no longer false-
fail with 401.

#2 sidecar swap in `templates_apply_pending` —
`commands_templates.rs` now rejects early when
`pending.automation_id != automation_id`. Without this, an
attacker (or a buggy caller) could pair automation A's pending-
changes.json with automation B's broader apply policy.

### Medium

#3 Ollama probe detection too broad — was `cfg.base_url.contains
("/api/")` which false-positives on `/api/v1` (OpenRouter,
LiteLLM). Now narrowed to `:11434` port OR explicit `/api/tags`
path.

#4 hardcoded `~/.claudepot` — both `templates_pending_changes`
and `templates_read_report` now use `claudepot_data_dir()` from
claudepot-core, honoring `CLAUDEPOT_DATA_DIR` overrides
correctly.

#5 `supported_platforms` was only enforced in `templates_list`
— `templates_install` now ALSO rejects when
`!bp.supports(HostPlatform::current())`. The contract is
symmetric: a direct IPC bypass of the gallery filter no longer
lets a macOS-only template instantiate on Linux/Windows.

#6 Windows ACL trusted `USERNAME` — `tighten_consent_acl` now
resolves the current user SID via `whoami /user /fo csv /nh`,
grants `*<SID>:F` in icacls (well-known SID notation), grants
SYSTEM via the literal `S-1-5-18` SID, and surfaces every
failure path through `tracing::warn` instead of swallowing
silently.

#7 cron test wrote `CLAUDE_OAUTH_TOKEN` to disk via
`automation.extra_env` (which `install_shim` renders into
`run.sh`). Token now lives in a 0600 sibling file
(`<tmp>/.oauth-token`); a 0700 wrapper script
(`<tmp>/claude-with-token.sh`) reads the file at fire time and
exec's the real claude. The shim sees only the wrapper path,
never the token.

#8 cron test scheduled `now + 1 minute`, which races the case
where `install_shim + scheduler.register` span past the next
minute boundary — launchd would defer to the same time
tomorrow and the test would hang. Now schedules `now + 2
minutes`; deadline extended to 180s.

#9 cron test passed on a `"result":` substring match, which is
too permissive — error rows mention `result` too. Now parses
`stdout.log` as a JSON event array, finds the terminal
`result` event, asserts `is_error == false`. Verified locally:
the cron run produced `is_error=false result_first_80=
CRON_TEMPLATE_OK`.

#10 `CronCleanupGuard` did not restore the previous
`CLAUDEPOT_DATA_DIR` — risk of polluting subsequent tests in
the same process. Now captures the prior value as
`Option<OsString>` at construction and restores it on Drop.
Also explicitly zero-fills + unlinks the token file so a
tempdir-removal race doesn't leave the token on disk.

### Low (deferred)

#11 validator rebuilds globset matcher per call. Apply is
interactive (~5-50 items × 1-3 globs each, user reviews + clicks
per run); not a hot loop. Caching would require an
`ApplyConfig`-level matcher cache and is not worth the
complexity. Documented in the audit-fix verification report.

### Verification

Independent Codex re-audit on a fresh thread:
| Status | Count |
|---|---:|
| FIXED | 10 |
| NOT FIXED | 0 |
| PARTIAL | 0 |
| REGRESSED | 0 |
| DEFERRED | 1 |

Local validation:
- `cargo test --workspace` — all green
- `cargo test -p claudepot-core --test templates_real_llm
   cron_schedule_fires -- --ignored` — passes with new
  is_error=false assertion (110s wall, ~$0.30)

Audit threadId: 019de662-808c-7b62-80b3-e85d373f92b5
Verify threadId: 019de792-3515-7533-aac0-0203a96388be
xiaolai added a commit that referenced this pull request May 8, 2026
fix(web): /c/[slug] 500 → 404 on unknown tag
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.

1 participant