Skip to content

fix: harden the capture pipeline for production use - #33

Merged
RubenGlez merged 3 commits into
mainfrom
fix/production-hardening
Aug 14, 2026
Merged

fix: harden the capture pipeline for production use#33
RubenGlez merged 3 commits into
mainfrom
fix/production-hardening

Conversation

@RubenGlez

Copy link
Copy Markdown
Owner

Fixes 21 correctness, performance and reliability issues found while auditing the library for real-world usage. Each fix ships with a regression test.

Silent data loss and hangs

Hono + Next.js App Router lost every request body Both read the body after the route handler had consumed it. Cloning a consumed Request throws, the catch swallowed it, and requestBody was recorded as null on every POST/PUT/PATCH. The existing Hono test passed only because its handler ignored the body.
Streaming responses hung the client Hono, Next.js and Elysia awaited .json() on a clone of the response before returning it. On an open SSE stream that never resolves, so the client received nothing.
Offline mode never worked The Ollama provider used the AI SDK's default OpenAI model, which targets /v1/responses. Ollama (and most OpenAI-compatible gateways) only implement /v1/chat/completions, so every generation failed — breaking both privacy.offline and the no-API-key fallback.
The circuit breaker never reopened Five consecutive failures disabled capture for the process lifetime, so a transient provider outage silently stopped all documentation until the next deploy. Now retries after a 60s cooldown.
NestJS never documented errors The interceptor used tap(), which only fires on success — every response from an exception filter went unrecorded.
Next.js Pages Router exploded endpoints Route params weren't separated from the query string, so /api/users/1 and /api/users/2 became separate rows, each paying for its own AI call.

Robustness

  • capture() can no longer throw into the host app's response path, and self-referential bodies terminate instead of overflowing the stack.
  • Concurrent workers (cluster mode, multiple replicas) no longer collide on the unique endpoint index after the AI call has been paid for — project and endpoint writes are atomic upserts.
  • Capture is bounded by default: 256 KB bodies, non-JSON responses skipped, 50 shapes per endpoint, 50 spec versions per endpoint. Previously maxBodySize had no default, so a stalled provider could let the queue retain unbounded payloads in the host app's heap.
  • Capturers expose flush(), drained on shutdown where the framework has a hook (Fastify onClose, Elysia onStop, NestJS onApplicationShutdown).
  • Default models updated to current, non-retired IDs; a rejected model now produces an explicit "pin ai.model" error rather than an opaque 404.

Performance

  • Repeated payload shapes are dropped synchronously, before the queue, so steady-state traffic neither retains bodies nor queues behind an in-flight generation.
  • The queue processes endpoints in parallel (still one shape at a time per endpoint), instead of one capture at a time globally.
  • The size cap stops as soon as the limit is exceeded rather than serializing the whole payload (~2ms → microseconds on a 1 MB body). Privacy rules are compiled once per config instead of per request.
  • The dashboard reuses one database handle; three API routes were opening a new client and re-running the schema DDL on every request.
  • Request bodies are trimmed like responses before going to the model, so a bulk payload is no longer billed in full.

Breaking

The Postgres helpers (createPgDB, pgGetAll, …) moved from the package root to @easydocs/core/storage/postgres, and postgres is now an optional peer dependency loaded on demand — the root re-export pulled the driver into every SQLite install. Postgres users must npm i postgres. Configuring storage.type: 'postgres' is unchanged.

Verification

  • build, lint, typecheck green.
  • 254 tests pass, up from 202. @easydocs/nextjs had no test setup at all and now has 7.
  • End-to-end run of real Express and Hono apps against a stub AI provider, asserting: request bodies reach the model, dynamic ids collapse to one endpoint, the SSE route returns in 0ms and creates no endpoint, repeated shapes cost exactly one generation, and no raw secret is stored.

Not addressed

pnpm audit --prod reports 2 pre-existing high CVEs (js-yaml, next > postcss > nanoid), both in apps/dashboard dependencies and untouched by this branch. They will block pnpm release until bumped.

https://claude.ai/code/session_01GrfumUQ4vFUAgsFYwzDyK8

Fixes 21 correctness, performance and reliability issues found while auditing
the library for real-world usage.

Silent data loss and hangs:
- Hono and Next.js App Router recorded `requestBody: null` on every POST/PUT/
  PATCH, because both read the body after the handler had consumed it.
- Streaming responses hung the client in the Hono, Next.js and Elysia adapters,
  which awaited `.json()` on an open stream before returning the response.
- Offline mode and the no-API-key Ollama fallback never worked: the AI SDK's
  default OpenAI model targets /v1/responses, which Ollama does not implement.
- The failure circuit breaker never reopened, so a transient provider outage
  stopped all documentation until the next deploy.
- NestJS never documented responses produced by exception filters.
- The Next.js Pages Router created one endpoint (and one AI call) per dynamic id.

Robustness:
- capture() can no longer throw into the host app's response path, and
  self-referential bodies terminate instead of overflowing the stack.
- Concurrent workers no longer collide on the unique endpoint index after the
  AI call has already been paid for.
- Capture is bounded by default: 256 KB bodies, non-JSON responses skipped,
  50 shapes per endpoint, 50 spec versions per endpoint.
- Capturers expose flush(), drained on shutdown where the framework has a hook.

Performance:
- Repeated payload shapes are dropped synchronously, before the queue, so
  steady-state traffic neither retains bodies nor queues behind a generation.
- The queue processes endpoints in parallel, one shape at a time per endpoint.
- The size cap stops as soon as the limit is exceeded rather than serializing
  the whole payload; privacy rules are compiled once per config.
- The dashboard reuses one database handle instead of opening a client and
  re-running the schema DDL per request.

BREAKING: the Postgres helpers moved to @easydocs/core/storage/postgres and
`postgres` is now an optional peer dependency, so SQLite installs no longer
pull in the driver. Configuring storage.type: 'postgres' is unchanged.

Verified with build, lint, typecheck and 254 tests (up from 202), plus an
end-to-end run of real Express and Hono apps against a stub AI provider.

Claude-Session: https://claude.ai/code/session_01GrfumUQ4vFUAgsFYwzDyK8
Every case spawns a fresh Node process to run the CLI bundle, which costs
over a second on a CI runner. Three of them shared one `it` and its 5s
default budget, which passed locally at ~2s and timed out at 5113ms in CI.

Claude-Session: https://claude.ai/code/session_01GrfumUQ4vFUAgsFYwzDyK8
@RubenGlez
RubenGlez merged commit a3e32cf into main Aug 14, 2026
2 of 3 checks passed
@RubenGlez
RubenGlez deleted the fix/production-hardening branch August 14, 2026 12:59
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