HarperFast / HarperFast/harper
Server HTTP request timeout silently aborts in-flight fetch() calls inside resource handlers
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Confusing behaviour
A POST handler that takes longer than the server's HTTP request timeout (~60s by default on a vanilla harper-pro tenant) gets its socket cut by the framework. That part is fine — what's confusing is that **any in-flight `fetch()` calls the handler is awaiting throw `AbortError: This operation was aborted`** rather than continuing to run.
The cause is that the per-request `AbortController` (correctly attached to the resource context for cancellation purposes) is also used as the default signal for `scope.models.{embed,generate}` and — via context propagation — to user-code `fetch()` calls that don't pass their own signal.
This is technically the right semantic ("the user gave up, so stop spending compute on them"), but for batch-style work that **needs** to outlive the request (e.g. "kick off a long-running import, persist progress to a table") the user gets the surprising result that their `await fetch(...)` throws halfway through with no other signal that the work was being cancelled.
## Repro
```js
// resources/SlowImport.js
export class SlowImport extends Resource {
async post(target, data) {
target.checkPermission = false
for (let i = 0; i < 200; i++) {
const res = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/Celebrity_${i}`)
// around iteration ~50, the HTTP server times out the original POST,
// and the next fetch throws AbortError instead of completing.
}
return { ok: true }
}
}
```
```
$ curl -X POST https://tenant/SlowImport -d '{}'
…(connection torn at 60s, response empty)…
server log:
[error] AbortError: This operation was aborted
at fetch (node:internal/deps/undici/undici:…)
at SlowImport.post (resources/SlowImport.js:6:23)
```
The user doesn't know whether to look at their code, the network, or Harper.
## What would fix it
Pick one (or both):
1. **Documentation.** Add a "Long-running work in handlers" section that says explicitly:
- "POST handlers that exceed `http.timeout` get their `AbortController` fired. This propagates to `scope.models.*` and any `fetch()` that didn't pass its own signal. To do work that survives the request, detach it from the request lifecycle (return immediately, kick the work off in `setImmediate` / a Promise without await, persist progress to a table)."
- Worked example with a fire-and-forget pattern.
2. **Bigger fix.** Separate "user-cancelled this request" from "the response socket has timed out". The first should fire the signal; the second arguably shouldn't, since the work the user requested is still wanted — they just won't see the response. Today both are conflated.
## Why this matters
Every long-running demo / import / batch job in user code hits this. I hit it in `harper-celebrity-match/ImportCelebrities.js` (had to switch to fire-and-forget after 33/193 imports landed and the rest aborted), and in `harper-roadmap/GenerateRoadmap.js` (the cross-cutting LLM pass kept dying at exactly 60s until I shrank the prompt).
The behaviour itself may be fine — what isn't fine is that there's no documentation about it and the error message ("This operation was aborted") doesn't say who's doing the aborting.
## Found in
harper-celebrity-match `resources/ImportCelebrities.js`, harper-roadmap `resources/GenerateRoadmap.js`.
Contributor guide
Assessment
This issue has not been assessed yet.