Skip to content

refactor(worker): Phase 4 — split hono-app.ts into domain route modules, remove bare-path double-mount#1443

Merged
jaypatrick merged 4 commits intomainfrom
copilot/structural-refactor-worker-hono-app
Mar 28, 2026
Merged

refactor(worker): Phase 4 — split hono-app.ts into domain route modules, remove bare-path double-mount#1443
jaypatrick merged 4 commits intomainfrom
copilot/structural-refactor-worker-hono-app

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

worker/hono-app.ts was a 1,279-line monolith mixing app setup, middleware, and every route domain inline. This PR extracts all route handlers into 10 domain-scoped sub-apps under worker/routes/, removes the legacy app.route('/', routes) bare-path double-mount, adds a CI guard preventing future route-ordering regressions, corrects Turnstile/zValidator middleware ordering issues in the extracted route modules, and merges the /api/auth/providers 404 fix from main.

Changes

  • worker/routes/ — new domain route modules

    File Routes
    shared.ts AppContext type + zodValidationError, verifyTurnstileInline, buildSyntheticRequest (extracted to avoid circular imports with hono-app.ts)
    compile.routes.ts /compile/*, /validate, /ast/parse, /ws/compile, /validate-rule
    rules.routes.ts /rules/*
    queue.routes.ts /queue/*
    configuration.routes.ts /configuration/*
    admin.routes.ts /admin/* (users, neon, agents, auth-config, usage, storage)
    monitoring.routes.ts /health/*, /metrics/*, /container/status
    api-keys.routes.ts /keys/*
    webhook.routes.ts /notify
    workflow.routes.ts /workflow/*
    browser.routes.ts Stub — no routes yet, ready for future PR
    index.ts Barrel exports
  • worker/hono-app.ts trimmed from 1,279 → ~547 lines; now app setup + global middleware only; route modules mounted via:

    routes.route('/', compileRoutes);
    routes.route('/', rulesRoutes);
    // ...etc
    app.route('/api', routes); // bare-path mount removed
  • Removed app.route('/', routes)/api is the sole canonical base path. MONITORING_BARE_PATHS constant retained (used by auth middleware for legacy request matching).

  • Corrected Turnstile/zValidator middleware ordering across compile.routes.ts and configuration.routes.ts:

    • /ast/parse, /validate — schemas have no turnstileToken field: turnstileMiddleware() moved before zValidator() so it reads from the unconsumed body clone; handlers updated to use buildSyntheticRequest(c, c.req.valid('json')).
    • /compile/async, /compile/batch/async, /compile/container — schemas include turnstileToken: replaced turnstileMiddleware() with verifyTurnstileInline() reading from c.req.valid('json'); handlers use buildSyntheticRequest. /compile/container body forwarding switched from c.req.raw.body to JSON.stringify(c.req.valid('json')) since zValidator consumes the stream.
    • /configuration/resolve — same inline Turnstile fix; turnstileMiddleware import removed from configuration.routes.ts.
  • Restored cache() middleware on GET /api/version and GET /api/schemas in hono-app.ts (was inadvertently dropped during extraction).

  • scripts/lint-route-order.ts — Deno script run in CI that enforces four invariants:

    1. timing() is the first app.use() call
    2. Better Auth /api/auth/* handler registered before agentRouter
    3. app.route('/', routes) double-mount is absent
    4. Compress middleware uses NO_COMPRESS_PATHS exclusion (not a bare wildcard)
  • deno.json — added lint:routes task (deno run --allow-read scripts/lint-route-order.ts)

  • .github/workflows/ci.ymlRoute-order lint step added to lint-format job

  • docs/architecture/hono-routing.md — Phase 4 section: new file layout, mount strategy, CI guard description; /api Prefix Handling section updated to document double-mount removal

  • worker/hono-app.test.ts — bare-path tests (e.g. GET /health, POST /compile) updated to use /api/ prefix; duplicate bare-path test cases removed where /api/ counterparts already existed

  • Merge conflict resolved — merged origin/main (PR fix: resolve /api/auth/providers HTTP 404 by adding pass-through in Better Auth wildcard #1442 /api/auth/providers 404 fix): Better Auth wildcard handler updated to async (c, next) signature with an explicit if (c.req.path === '/api/auth/providers') return next() pass-through; /api/auth/providers registration moved to after the meta-routes section with a full explanatory comment.

Testing

  • Unit tests added/updated — bare-path tests migrated to /api/ prefix; no net test removal (1,189 passed)
  • Manual testing performed — deno task lint:routes all 4 checks pass; deno lint + deno fmt --check clean
  • CI passes

Zero Trust Architecture Checklist

Worker / Backend

  • Every handler verifies auth before executing business logic — N/A (routing-layer refactor; no auth logic changed)
  • CORS origin allowlist enforced (not *) on write/authenticated endpoints — N/A (CORS middleware unchanged)
  • All secrets accessed via Worker Secret bindings (not [vars]) — N/A (no secret access added)
  • All external inputs Zod-validated before use — all existing zValidator calls moved verbatim; Turnstile/zValidator ordering corrected so body is never double-read or consumed before verification
  • All D1 queries use parameterized .prepare().bind() (no string interpolation) — N/A (no queries added)
  • Security events emitted to Analytics Engine on auth failures — N/A (no auth logic changed)

Frontend / Angular

  • Protected routes have functional CanActivateFn auth guards — N/A
  • Auth tokens managed via Clerk SDK (not localStorage) — N/A
  • HTTP interceptor attaches ****** (no manual token passing) — N/A
  • API responses validated with Zod schemas before consumption — N/A
Original prompt

PR A — Structural Refactor (must merge before PR B / tRPC)

This PR performs a pure structural refactor of worker/hono-app.ts with zero behaviour changes. It is a prerequisite for the tRPC integration PR.

Background

worker/hono-app.ts is a ~1,300-line monolith that currently:

  • Declares every route domain (compile, rules, admin, queue, workflow, health, config, etc.) inline
  • Double-mounts the routes sub-app at both /api and / (app.route('/api', routes); app.route('/', routes)) — the bare / mount is legacy and should be removed
  • Has no CI guard preventing future middleware/route ordering regressions

Goals

  1. Split hono-app.ts into domain-scoped route files — one Hono OpenAPIHono sub-app per domain, mounted cleanly in hono-app.ts
  2. Remove the app.route('/', routes) bare-path double-mount — keep only app.route('/api', routes)
  3. Add a CI lint script that detects route-ordering regressions (auth middleware registered after route handlers, compress registered before monitoring exclusions, etc.)
  4. Update docs/architecture/hono-routing.md to reflect the new file layout

Required file layout after this PR

worker/
  hono-app.ts                      ← kept, but trimmed to app setup + middleware only; imports route modules
  routes/
    compile.routes.ts              ← all /compile/*, /validate, /ast routes
    rules.routes.ts                ← all /rules/* routes
    queue.routes.ts                ← all /queue/* routes
    configuration.routes.ts        ← all /configuration/* routes
    admin.routes.ts                ← all /admin/* routes (users, neon, agents, storage, etc.)
    monitoring.routes.ts           ← /health, /health/latest, /health/db-smoke, /metrics, /metrics/prometheus
    api-keys.routes.ts             ← all /api-keys/* routes
    webhook.routes.ts              ← /webhooks route
    workflow.routes.ts             ← /workflow/* routes
    browser.routes.ts              ← /browser/* routes (monitor-latest, url-resolver, source-monitor)
    index.ts                       ← barrel: exports all sub-apps so hono-app.ts imports one file

Each route file exports a single OpenAPIHono<{ Bindings: Env; Variables: Variables }> instance.

Domain → handler mapping

Pull each domain's routes out of hono-app.ts exactly as they are (no logic changes). The mapping is:

Route file Routes from hono-app.ts
compile.routes.ts All routes calling handleCompileJson, handleCompileStream, handleCompileBatch, handleCompileAsync, handleCompileBatchAsync, handleValidate, handleASTParseRequest, handleValidateRule
rules.routes.ts All routes calling handleRulesList, handleRulesGet, handleRulesCreate, handleRulesUpdate, handleRulesDelete
queue.routes.ts The routes.all('/queue/*', ...) handler
configuration.routes.ts All routes calling handleConfigurationDefaults, handleConfigurationResolve, handleConfigurationValidate
admin.routes.ts All routes calling admin-users, admin-neon, admin-agents, auth-config, auth-admin, admin-usage, admin-storage handlers; also includes /admin/pg/* (pg-admin), /admin/migrate/* (migrate), and /admin/containers/* (container-status) routes
monitoring.routes.ts /health, /health/latest, /health/db-smoke, /metrics, /metrics/prometheus — these MUST keep the NO_COMPRESS_PATHS exclusion and res.text() body materialisation from the current main branch
api-keys.routes.ts All routes calling handleCreateApiKey, handleListApiKeys, handleRevokeApiKey, handleUpdateApiKey
webhook.routes.ts The webhook route calling handleNotify
workflow.routes.ts The routes.all('/workflow/*', ...) handler
browser.routes.ts Routes calling handleMonitorLatest, handleResolveUrl, handleSourceMonitor

Updated hono-app.ts after extraction

The trimmed hono-app.ts should only contain:

  1. All imports (types, middleware, utilities) — unchanged
  2. Global middleware chain (timing, request metadata, SSR detection, Better Auth handler, agent router, unified auth+rate-limit, CORS, secureHeaders, prettyJSON) — unchanged and in the same order
  3. Pre-auth route declarations (app.get for /api, /api/version, /api/schemas, /api/deployments, /api/turnstile-config, /api/sentry-config, /api/auth/providers) — unchanged
  4. The routes sub-app declaration + its middleware (logger, compress-with-exclusions, ZTA permission check)
  5. Import and mount of each domain route module:
    import { compileRoutes } from './routes/compile.routes.ts';
    import { rulesRoutes } from './routes/rules.routes.ts';
    // ... etc
    routes.route('/', compileRoutes);
    routes.route('/', rulesRoutes);
    // ... etc
  6. Mount the routes sub-app: app.route('/api', routes);remove app.route('/', routes);
  7. The existing handleRequest export at the bottom — unchanged

CI...

This pull request was created from Copilot chat.


⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

@jaypatrick jaypatrick added enhancement New feature or request frontend Angular frontend labels Mar 28, 2026
@jaypatrick jaypatrick added this to the beta milestone Mar 28, 2026
- Extract all route handlers from hono-app.ts (1279 lines) into 10 domain-scoped
  route files under worker/routes/
- Remove app.route('/', routes) bare-path double-mount — /api is canonical
- Add scripts/lint-route-order.ts CI guard for route ordering invariants
- Add lint:routes task to deno.json and CI step to ci.yml
- Update docs/architecture/hono-routing.md with Phase 4 section
- Update hono-app.test.ts bare-path tests to use /api/ prefix

Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/86d24908-49bb-4286-87b5-ee18aedaa606

Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor structure of worker/hono-app.ts with no behavior changes refactor(worker): Phase 4 — split hono-app.ts into domain route modules, remove bare-path double-mount Mar 28, 2026
Copilot AI requested a review from jaypatrick March 28, 2026 04:22
@jaypatrick jaypatrick requested a review from Copilot March 28, 2026 04:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the Worker’s Hono application by extracting route domains into dedicated worker/routes/* modules, removing the legacy bare-path (/) double-mount so /api becomes the single canonical base path, and adding a CI guard to prevent middleware/route-order regressions.

Changes:

  • Split route handlers into domain-scoped OpenAPIHono sub-app modules under worker/routes/ and mount them from worker/hono-app.ts.
  • Remove app.route('/', routes) so only /api/* routes are served; update Worker tests and routing docs accordingly.
  • Add scripts/lint-route-order.ts and wire it into deno.json tasks and CI.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
worker/routes/shared.ts New shared Variables/AppContext and shared helpers used by route modules.
worker/routes/compile.routes.ts New compile/validate/WS routes module extracted from hono-app.ts.
worker/routes/rules.routes.ts New rules CRUD routes module.
worker/routes/queue.routes.ts New lazy-loaded queue routes module.
worker/routes/configuration.routes.ts New configuration routes module (defaults/validate/resolve).
worker/routes/admin.routes.ts New admin routes module + extracted session revocation handler.
worker/routes/monitoring.routes.ts New health/metrics/container status routes module.
worker/routes/api-keys.routes.ts New API key management routes module.
worker/routes/webhook.routes.ts New webhook notification route module.
worker/routes/workflow.routes.ts New lazy-loaded workflow routes module.
worker/routes/browser.routes.ts Stub module for future browser routes.
worker/routes/index.ts Barrel exports for route modules and shared helpers/types.
worker/hono-app.ts Trimmed app setup; mounts extracted route modules; removes bare-path double-mount.
worker/hono-app.test.ts Updates tests to use /api/* prefix and removes redundant bare-path cases.
scripts/lint-route-order.ts Adds CI lint enforcing key route/middleware ordering invariants.
docs/architecture/hono-routing.md Documents new module layout and /api-only mount strategy.
deno.json Adds lint:routes task.
.github/workflows/ci.yml Adds route-order lint step to CI.
deno.lock Updates lockfile for new script dependency resolution.
Comments suppressed due to low confidence (1)

worker/hono-app.ts:99

  • monitoringRoutes registers /container/status (served as /api/container/status), but MONITORING_API_PATHS (and therefore PRE_AUTH_PATHS) doesn’t include /api/container/status. That means this endpoint won’t get the intended pre-auth bypass path and will go through the full unified-auth chain on every poll, despite being Anonymous-tier in ROUTE_PERMISSION_REGISTRY. Consider adding /api/container/status to MONITORING_API_PATHS (or otherwise including it in the pre-auth bypass set).
// Dashboard monitoring endpoints — read-only, no PII, publicly accessible by design.
const MONITORING_API_PATHS = [
    '/api/health',
    '/api/health/latest',
    '/api/health/db-smoke',
    '/api/metrics',
] as const;

@jaypatrick
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…restore cache middleware

- compile.routes.ts:
  - /ast/parse, /validate: move turnstileMiddleware() BEFORE zValidator() so
    it reads from the unconsumed body clone; update handlers to use
    buildSyntheticRequest(c, c.req.valid('json')) since zValidator now consumes
    the body after Turnstile runs
  - /compile/async, /compile/batch/async: replace turnstileMiddleware() with
    verifyTurnstileInline() pattern (reads turnstileToken from c.req.valid('json'))
    and use buildSyntheticRequest to avoid re-reading consumed stream
  - /compile/container: same inline Turnstile fix; replace c.req.raw.body with
    JSON.stringify(c.req.valid('json')) since body is consumed by zValidator
- configuration.routes.ts:
  - /configuration/resolve: replace turnstileMiddleware() with verifyTurnstileInline()
    + buildSyntheticRequest; remove now-unused turnstileMiddleware import
- hono-app.ts: restore cache() middleware on /api/version and /api/schemas

Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/70cde4c8-0bba-4bdc-99ee-e96fd7d5300e

Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in 95eb0eb:

compile.routes.ts

  • /ast/parse, /validate — schemas have no turnstileToken field, so turnstileMiddleware() is now moved before zValidator() (reads from c.req.raw.clone() while body is still unconsumed). Handlers updated to use buildSyntheticRequest(c, c.req.valid('json')) since zValidator now consumes the body after Turnstile.
  • /compile/async, /compile/batch/async — switched from turnstileMiddleware() to verifyTurnstileInline() reading c.req.valid('json').turnstileToken; handlers use buildSyntheticRequest.
  • /compile/container — same inline Turnstile fix; body forwarded to container DO now uses JSON.stringify(c.req.valid('json')) instead of the consumed c.req.raw.body.

configuration.routes.ts

  • /configuration/resolve — replaced turnstileMiddleware() with verifyTurnstileInline() + buildSyntheticRequest; removed now-unused turnstileMiddleware import.

hono-app.ts

  • Restored cache({ cacheName: 'api-version', ... }) and cache({ cacheName: 'api-schemas', ... }) on /api/version and /api/schemas.

@jaypatrick
Copy link
Copy Markdown
Owner

@copilot Please resolve the merge conflict and be aware of PR 1444 which will be merged after this PR.

- Take origin/main version for Better Auth wildcard handler: adds (c, next)
  signature and explicit pass-through for /api/auth/providers so the custom
  endpoint receives full CORS + rate-limiting middleware coverage
- Take origin/main version for /api/auth/providers registration comment
  (explains the post-meta-section placement and wildcard pass-through)

Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot Please resolve the merge conflict and be aware of PR 1444 which will be merged after this PR.

Merge conflict resolved in 86b3706. The only conflicting file was worker/hono-app.ts — both conflict hunks were resolved by taking the origin/main version from PR #1442:

  1. Better Auth wildcard handler — updated from async (c) to async (c, next) signature, with the explicit if (c.req.path === '/api/auth/providers') return next() pass-through and the full explanatory comment block.
  2. /api/auth/providers registration — kept with the comment explaining why it's registered after the meta-routes section and relies on the wildcard pass-through.

All 1189 tests pass, lint and fmt clean.

@jaypatrick jaypatrick requested a review from Copilot March 28, 2026 06:03
@jaypatrick jaypatrick marked this pull request as ready for review March 28, 2026 06:09
@jaypatrick jaypatrick merged commit 731a383 into main Mar 28, 2026
33 checks passed
@jaypatrick jaypatrick deleted the copilot/structural-refactor-worker-hono-app branch March 28, 2026 06:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 11 comments.

'/admin/neon/query',
// deno-lint-ignore no-explicit-any
zValidator('json', AdminNeonQuerySchema as any, zodValidationError),
(c) => handleAdminNeonQuery(c.req.raw, c.env, c.get('authContext')),
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

handleAdminNeonQuery() calls request.json() internally, but this route runs zValidator('json', ...) before invoking it and passes c.req.raw. If zValidator consumes the body stream (as documented in this repo), the handler can see an empty/consumed body and fail.

Fix by removing zValidator or by passing a synthetic Request rebuilt from c.req.valid('json').

Suggested change
(c) => handleAdminNeonQuery(c.req.raw, c.env, c.get('authContext')),
(c) => {
const body = c.req.valid('json');
const reqWithBody = new Request(c.req.raw, {
body: JSON.stringify(body),
});
return handleAdminNeonQuery(reqWithBody, c.env, c.get('authContext'));
},

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +39
// Strip single-line and multi-line comments so we don't accidentally match
// commented-out code. This is intentionally lightweight — it does not handle
// every edge case, but is sufficient for the ordered-invariant checks below.
const srcNoComments = src
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
.replace(/\/\/.*/g, ''); // line comments
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The lint script removes //.* to strip comments, but that also matches // inside string literals (e.g. https://... URLs in hono-app.ts). This can corrupt srcNoComments and make the invariant checks unreliable or flaky as the file evolves.

Consider using a small tokenizer/AST-based approach (e.g. deno_ast/TypeScript compiler API) or a safer comment stripper that ignores strings/template literals.

Suggested change
// Strip single-line and multi-line comments so we don't accidentally match
// commented-out code. This is intentionally lightweight — it does not handle
// every edge case, but is sufficient for the ordered-invariant checks below.
const srcNoComments = src
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
.replace(/\/\/.*/g, ''); // line comments
/**
* Strip single-line and multi-line comments in a way that preserves strings
* and template literals, so that `//` inside literals (e.g. URLs) do not get
* mistaken for comments.
*/
function stripComments(source: string): string {
let result = '';
let inSingleQuote = false;
let inDoubleQuote = false;
let inTemplate = false;
let inLineComment = false;
let inBlockComment = false;
let prevChar = '';
for (let i = 0; i < source.length; i++) {
const char = source[i]!;
const nextChar = i + 1 < source.length ? source[i + 1]! : '';
// Handle end of line comment
if (inLineComment) {
if (char === '\n') {
inLineComment = false;
result += char;
}
continue;
}
// Handle end of block comment
if (inBlockComment) {
if (char === '*' && nextChar === '/') {
inBlockComment = false;
i++; // Skip '/'
}
continue;
}
// Inside a template literal
if (inTemplate) {
result += char;
if (char === '`' && prevChar !== '\\') {
inTemplate = false;
}
prevChar = char === '\\' && prevChar === '\\' ? '' : char;
continue;
}
// Inside a single-quoted string
if (inSingleQuote) {
result += char;
if (char === "'" && prevChar !== '\\') {
inSingleQuote = false;
}
prevChar = char === '\\' && prevChar === '\\' ? '' : char;
continue;
}
// Inside a double-quoted string
if (inDoubleQuote) {
result += char;
if (char === '"' && prevChar !== '\\') {
inDoubleQuote = false;
}
prevChar = char === '\\' && prevChar === '\\' ? '' : char;
continue;
}
// Not currently in string/template/comment: check for start of comment or string
if (char === '/' && nextChar === '/') {
inLineComment = true;
i++; // Skip second '/'
continue;
}
if (char === '/' && nextChar === '*') {
inBlockComment = true;
i++; // Skip '*'
continue;
}
if (char === "'") {
inSingleQuote = true;
result += char;
prevChar = char;
continue;
}
if (char === '"') {
inDoubleQuote = true;
result += char;
prevChar = char;
continue;
}
if (char === '`') {
inTemplate = true;
result += char;
prevChar = char;
continue;
}
// Regular character
result += char;
prevChar = char === '\\' && prevChar === '\\' ? '' : char;
}
return result;
}
// Strip single-line and multi-line comments so we don't accidentally match
// commented-out code while preserving strings and template literals.
const srcNoComments = stripComments(src);

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +44
rulesRoutes.post(
'/rules',
requireAuthMiddleware(),
bodySizeMiddleware(),
rateLimitMiddleware(),
// deno-lint-ignore no-explicit-any
zValidator('json', RuleSetCreateSchema as any, zodValidationError),
(c) => handleRulesCreate(c.req.raw, c.env),
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

This route uses zValidator('json', ...) but then forwards c.req.raw to handleRulesCreate(), which reads request.json() internally. Per the routing docs in this repo, zValidator consumes the original body stream, so the handler may see an empty/consumed body and fail.

Fix by removing zValidator (handler already validates) or by rebuilding a synthetic Request from c.req.valid('json') before calling the handler.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +60
rulesRoutes.put(
'/rules/:id',
requireAuthMiddleware(),
bodySizeMiddleware(),
rateLimitMiddleware(),
// deno-lint-ignore no-explicit-any
zValidator('json', RuleSetUpdateSchema as any, zodValidationError),
(c) => handleRulesUpdate(c.req.param('id')!, c.req.raw, c.env),
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

Same body-consumption issue as POST /rules: zValidator('json', ...) runs before calling handleRulesUpdate(), and that handler reads request.json() internally. Passing c.req.raw after zValidator can cause the handler to fail on a consumed body.

Fix by removing zValidator or passing a synthetic Request built from c.req.valid('json').

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +34
// deno-lint-ignore no-explicit-any
zValidator('json', WebhookNotifyRequestSchema as any, zodValidationError),
(c) => handleNotify(c.req.raw, c.env),
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

This route runs zValidator('json', WebhookNotifyRequestSchema, ...) and then calls handleNotify(c.req.raw, ...), but handleNotify() reads request.json() internally. If zValidator consumes the body stream (as documented in this repo), the handler may fail to parse JSON.

Fix by dropping zValidator (handler already validates) or by creating a synthetic Request from c.req.valid('json') and passing that to handleNotify.

Suggested change
// deno-lint-ignore no-explicit-any
zValidator('json', WebhookNotifyRequestSchema as any, zodValidationError),
(c) => handleNotify(c.req.raw, c.env),
);
(c) => handleNotify(c.req.raw, c.env),
);

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +171
adminRoutes.post(
'/admin/neon/branches',
// deno-lint-ignore no-explicit-any
zValidator('json', AdminNeonCreateBranchSchema as any, zodValidationError),
(c) => handleAdminNeonCreateBranch(c.req.raw, c.env, c.get('authContext')),
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

handleAdminNeonCreateBranch() reads request.json() internally, but this route runs zValidator('json', ...) first and then passes c.req.raw to the handler. Per the routing docs in this repo, zValidator consumes the original body stream, so the handler can fail to parse the body.

Fix by removing zValidator (handler already validates) or by passing a synthetic Request rebuilt from c.req.valid('json').

Suggested change
adminRoutes.post(
'/admin/neon/branches',
// deno-lint-ignore no-explicit-any
zValidator('json', AdminNeonCreateBranchSchema as any, zodValidationError),
(c) => handleAdminNeonCreateBranch(c.req.raw, c.env, c.get('authContext')),
adminRoutes.post('/admin/neon/branches', (c) =>
handleAdminNeonCreateBranch(c.req.raw, c.env, c.get('authContext')),

Copilot uses AI. Check for mistakes.
rateLimitMiddleware(),
// deno-lint-ignore no-explicit-any
zValidator('json', ValidateRuleRequestSchema as any, zodValidationError),
(c) => handleValidateRule(c.req.raw, c.env),
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

zValidator() runs before the handler, and the documentation for this project states it consumes the original request body stream. handleValidateRule() then calls request.json() internally, so passing c.req.raw here can result in an empty/consumed body and a 400/failed validation.

Fix by either (a) removing zValidator for this route (the handler already validates with Zod), or (b) passing a synthetic Request built from c.req.valid('json') (as done for other compile routes).

Suggested change
(c) => handleValidateRule(c.req.raw, c.env),
(c) => handleValidateRule(buildSyntheticRequest(c, c.req.valid('json')), c.env),

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +51
apiKeysRoutes.post(
'/keys',
requireAuthMiddleware(),
rateLimitMiddleware(),
// deno-lint-ignore no-explicit-any
zValidator('json', CreateApiKeyRequestSchema as any, zodValidationError),
async (c) => {
if (!INTERACTIVE_AUTH_METHODS.has(c.get('authContext').authMethod)) return JsonResponse.forbidden('API key management requires an authenticated user session');
if (!c.env.HYPERDRIVE) return JsonResponse.serviceUnavailable('Database not configured');
return handleCreateApiKey(c.req.raw, c.get('authContext'), c.env.HYPERDRIVE.connectionString, createPgPool);
},
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

zValidator('json', CreateApiKeyRequestSchema, ...) runs before calling handleCreateApiKey(c.req.raw, ...), but handleCreateApiKey() reads request.json() internally. With the documented zValidator body consumption semantics, passing c.req.raw risks a consumed/empty body.

Fix by removing zValidator for this route or by passing a synthetic Request rebuilt from c.req.valid('json').

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +85
apiKeysRoutes.patch(
'/keys/:id',
requireAuthMiddleware(),
rateLimitMiddleware(),
// deno-lint-ignore no-explicit-any
zValidator('json', UpdateApiKeyRequestSchema as any, zodValidationError),
async (c) => {
if (!INTERACTIVE_AUTH_METHODS.has(c.get('authContext').authMethod)) return JsonResponse.forbidden('API key management requires an authenticated user session');
if (!c.env.HYPERDRIVE) return JsonResponse.serviceUnavailable('Database not configured');
return handleUpdateApiKey(c.req.param('id')!, c.req.raw, c.get('authContext'), c.env.HYPERDRIVE.connectionString, createPgPool);
},
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

Same issue as POST /keys: this route runs zValidator('json', UpdateApiKeyRequestSchema, ...) and then calls handleUpdateApiKey(..., c.req.raw, ...), but the handler reads request.json() internally. If zValidator consumes the body stream, the handler can fail.

Fix by removing zValidator or by rebuilding a synthetic Request from c.req.valid('json') and passing that to the handler.

Copilot uses AI. Check for mistakes.
'/admin/users/:id/ban',
// deno-lint-ignore no-explicit-any
zValidator('json', AdminBanUserSchema as any, zodValidationError),
(c) => handleAdminBanUser(c.req.raw, c.env, c.get('authContext'), c.req.param('id')!),
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

This route runs zValidator('json', AdminBanUserSchema, ...) and then calls handleAdminBanUser(c.req.raw, ...), but handleAdminBanUser() reads the request body (request.text() / JSON.parse). If zValidator consumes the body stream, the handler will see an empty body and fail.

Fix by removing zValidator for this route or by passing a synthetic Request rebuilt from c.req.valid('json').

Suggested change
(c) => handleAdminBanUser(c.req.raw, c.env, c.get('authContext'), c.req.param('id')!),
(c) => {
const validatedBody = c.req.valid('json');
const syntheticRequest = new Request(c.req.raw, {
body: JSON.stringify(validatedBody),
headers: c.req.raw.headers,
});
return handleAdminBanUser(syntheticRequest, c.env, c.get('authContext'), c.req.param('id')!);
},

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request frontend Angular frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants