apollographql / apollographql/apollo-client-integrations

react-router: query slower than `streamTimeout` truncates the transported queryRef and fails hydration with a misleading `InvariantViolation`

Open
#563 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
556
Forks
53
PR merge metrics
No merged PRs in 30d

Description

## Summary

When a query preloaded via `preloadQuery` in an `apolloLoader` takes longer than React Router's `streamTimeout`, React Router aborts the turbo-stream encode. The transported query ref is truncated, so on the client the Apollo link chain completes without ever emitting a value, and Apollo throws:

```
Invariant Violation: The link chain completed without emitting a value.
This is likely unintentional and should be updated to emit a value before completing.
```

The error propagates to the route's `ErrorBoundary`, so the user gets an error page — replacing HTML that had already rendered correctly on the server.

**Minimal reproduction:** https://github.com/thelooter/apollo-rr-loader-repro

## Reproduction

```bash
git clone https://github.com/thelooter/apollo-rr-loader-repro
cd apollo-rr-loader-repro

# terminal 1 — mock GraphQL API, slower than streamTimeout
cd mock-api && DELAY_MS=6000 node server.mjs

# terminal 2
cd rr8 && pnpm install && pnpm dev
```

Open the page: `loading…`, then **Oops!** with the invariant violation.

Set `DELAY_MS=2000`, restart the API, reload — renders and hydrates cleanly. Or keep `DELAY_MS=6000` and run `STREAM_TIMEOUT=30000 pnpm dev`, which also works.

Both apps are wired straight from the package README. The mock API returns canned JSON with no resolver work, so `DELAY_MS` *is* the query latency.

## Results

| Query latency | React Router 7.18.2 | React Router 8.3.0 |
|---|---|---|
| **2000ms** (under `streamTimeout`) | ✅ TTFB 0.016s, data streams in, hydrates clean | ✅ TTFB 0.008s, data streams in, hydrates clean |
| **6000ms** (over `streamTimeout`) | ❌ fallback-only HTML, then `link chain completed` + error boundary | ❌ identical |

Under the timeout everything behaves exactly as documented — the shell flushes immediately with the Suspense fallback and the data streams into the same response. **Not React Router 8-specific:** 7.18.2 fails identically, so this isn't a v8 regression.

## Mechanism

`encodeViaTurboStream` in `react-router/dist/.../server-runtime/single-fetch.js`:

```js
let timeoutId = setTimeout(() => {
controller.abort(new Error("Server Timeout"));
cleanupCallbacks();
}, typeof streamTimeout === "number" ? streamTimeout : 4950);
```

Server-side you get an unhandled `Error: Server Timeout` from that line; client-side, the truncated stream means `ReadFromReadableStreamLink` sees completion with no value.

## Why I think it's worth addressing

Exceeding `streamTimeout` failing is arguably correct behaviour. The problem is the shape of the failure:

- The error names none of the actual causes — not `streamTimeout`, not turbo-stream, not the slow query. "The link chain completed without emitting a value" reads like an Apollo Link misconfiguration and sends you looking in entirely the wrong place.
- It arrives as an unhandled render error rather than a timeout, so there's no clean way to catch it and fall back to a client-side fetch.
- The threshold is invisible. `react-router reveal` scaffolds `streamTimeout = 5_000` and nothing connects that number to Apollo, so the first encounter is typically a blank error page in production on your slowest route.

A targeted error when the transported stream is aborted mid-flight — naming `streamTimeout` and suggesting raising it — would turn a multi-hour debugging session into a one-line fix.

## Versions

`@apollo/client-integration-react-router` 0.14.5-alpha.0, `@apollo/client` 4.2.6, react/react-dom 19.2.7, react-router 8.3.0 and 7.18.2, Node 24.19.0.

Contributor guide

No contributing guide indexed for this repository

Research direction

Reproduce the failure with mock-api/server.mjs and the rr8 app, varying DELAY_MS and STREAM_TIMEOUT. Inspect encodeViaTurboStream in react-router's server-runtime/single-fetch.js and the ReadFromReadableStreamLink completion path; done means an aborted transported stream produces a clear timeout-related error rather than the misleading invariant violation.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, node.js, react, typescript
Domain
api, backend, full-stack
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.