electric-sql / electric-sql/electric

Bug: Client treats 'offset out of bounds' (400) as fatal — should auto-recover like 409

Open
#3,899 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
10.4k
Forks
375
Avg merge
3d 1h
Merged PRs (30d)
18

Description

## Problem

When the Electric server returns a 400 with `{"errors": {"offset": ["out of bounds for this shape"]}}`, the TypeScript client treats it as an unrecoverable error. The error is thrown from `#requestShape`, caught by `#start`, and either passed to `onError` or sent to error subscribers — terminating the stream.

However, "offset out of bounds" is a **recoverable** situation. The client's offset is ahead of (or no longer within) the server's known range for the shape. The correct recovery is the same as a 409: reset the shape and refetch from scratch.

## Current behavior

`packages/typescript-client/src/client.ts` line ~883:
```typescript
} else {
// errors that have reached this point are not actionable without
// additional user input, such as 400s or failures to read the
// body of a response, so we exit the loop and let #start handle it
throw e
}
```

All non-409 `FetchError`s — including "offset out of bounds" — hit this branch and are thrown as fatal.

## Expected behavior

When the client receives a 400 with `offset: ["out of bounds for this shape"]`, it should:
1. Reset the shape state (clear handle, offset back to `-1`)
2. Refetch from scratch — same as 409 recovery

This is equivalent to saying "I don't know where I am in this shape's log anymore, start over."

## Customer impact

A customer running 20+ shapes saw this error after applying a template that created many rows in a single transaction. The stream died and did not recover. See #3898 for the likely server-side root cause.

## Proposed fix

In `#requestShape`'s error handling, detect the "offset out of bounds" 400 and treat it like a 409:

```typescript
if (e.status === 400 && e.json?.errors?.offset?.includes('out of bounds for this shape')) {
this.#reset()
return this.#requestShape()
}
```

Alternatively, a more general approach: treat any 400 with an `offset` error as a signal to refetch.

## Related
- #3900 — Server-side: Chunk boundary race exposes mid-transaction offsets
- #3896 — Backoff defaults improvement (reduces retry storm impact)

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.