HarperFast / HarperFast/harper

Server HTTP request timeout silently aborts in-flight fetch() calls inside resource handlers

Open
#774 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.