skillguard/.agents/memory/ndjson-streaming-express-replit.md
Replit Agent 434ec07885 Add live progress updates and detailed scan checkpoints to scan results
Introduce streaming endpoint for NDJSON scan progress, incorporate scan checkpoints into scan details, and update UI components to display this new information.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 0d01f99a-ea6a-447d-82fd-311715434a39
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2852b526-3bf8-4a93-a62a-a50e26291074
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/e32d2b99-1721-47dd-833c-98b372f48008/0d01f99a-ea6a-447d-82fd-311715434a39/8MCgDZm
Replit-Helium-Checkpoint-Created: true
2026-06-10 18:53:17 +00:00

36 lines
1.9 KiB
Markdown

---
name: NDJSON streaming under Express behind the Replit proxy
description: Pitfalls for live/streaming HTTP responses (NDJSON/SSE) from an Express API on Replit — client-disconnect detection, persistence, and proxy buffering
---
# Streaming responses (NDJSON) from Express on Replit
For a long-lived POST that streams progress (e.g. `application/x-ndjson` with
`res.flushHeaders()` then line-delimited JSON writes):
**Detect a real client abort with `res.on("close")` + `res.writableFinished` — never
`req.on("close")`.**
`req.on("close")` fires as soon as the POST *request body* has been fully consumed,
which is normal and happens immediately. Gating writes on that wrongly suppresses the
entire stream. Use the *response* close event, and treat it as an abort only when
`!res.writableFinished`.
**Persist the result regardless of disconnect.** Run the analysis and the DB write
inside the handler's `try` unconditionally; only guard the `res.write()` calls on
"connection still open". This way a client that disconnects mid-stream still gets its
result persisted (and the client must NOT re-submit on a mid-stream error, or it
creates a duplicate row — see below).
**The Replit path-proxy does NOT buffer the stream.** Incremental flushes arrive line
by line through `$REPLIT_DEV_DOMAIN`, same as `localhost`. No special proxy header or
chunk-padding needed.
**Client fetch must be root-relative (`/api/...`), not `import.meta.env.BASE_URL`.**
That matches the generated API client and is what routes correctly through the proxy.
**Fallback gating to avoid duplicate persists:** a client that streams and also falls
back to a non-streaming POST on error must only fall back when the server never
processed the request (fetch rejected, or `!res.ok` with a 5xx). Once the response
body has started streaming, any read error means the server is already persisting —
falling back then duplicates the record. Distinguish 4xx (show message, no retry)
from transport failures.