JuliusBrussee/caveman · error · MiddlewareError

invalid_recovery

invalid_recovery

Error message

invalid_recovery

What it means

MiddlewareError 'invalid_recovery' is thrown by validatePage when a RecoveryPage fails a structural check: schema_version, handle matching the RetrieveArgs, token-shaped source_id, hash-shaped original_sha256, string text within maxBytes, integer total_bytes/offset matching the requested offset, allowed kind ('original_page'|'excerpt'), boolean complete, and next_offset either null or an integer. It is the first of several recovery-page integrity gates before text is returned to the caller.

Solutions

  1. Ensure the page's handle exactly equals args.handle and offset equals args.offset ?? 0
  2. Fix the source so kind is 'original_page' or 'excerpt' and next_offset is null or an integer
  3. Check total_bytes/offset are integers and text byte length does not exceed maxBytes
  4. Regenerate the page from storage instead of replaying a cached one
  5. Verify SDK/storage adapter versions agree on the RecoveryPage schema

Example fix

// before
const page = await store.fetch(anyHandle, requestedOffset + 1);
// after: echo back exactly the requested handle/offset
const page = await store.fetch(args.handle, args.offset ?? 0);
Defensive patterns

Strategy: validation

Validate before calling

function precheckPage(p, args) {
  return p && p.schema_version === 1 && p.handle === args.handle &&
    typeof p.source_id === 'string' && typeof p.original_sha256 === 'string' &&
    typeof p.text === 'string' && Number.isInteger(p.total_bytes) && Number.isInteger(p.offset) &&
    p.offset === (args.offset ?? 0) &&
    ['original_page','excerpt'].includes(p.kind) &&
    (p.next_offset === null || Number.isInteger(p.next_offset));
}
if (!precheckPage(page, args)) throw new Error('recovery page fails local precheck');

Type guard

const isRecoveryPage = (p) => !!p && p.schema_version === 1 && typeof p.handle === 'string' &&
  typeof p.text === 'string' && (p.kind === 'original_page' || p.kind === 'excerpt') &&
  typeof p.complete === 'boolean' && (p.next_offset === null || Number.isInteger(p.next_offset));

Try / catch

try { const page = await retrieve(args); } catch (e) {
  if (e.code === 'invalid_recovery') {
    // refetch from canonical storage; do not serve unvalidated recovery data
    cache.invalidate(args.handle);
    page = await retrieve(args);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling validatePage (via retrieve) with a page whose handle differs from args.handle, offset not equal to args.offset ?? 0, kind outside the two allowed values, non-integer or negative total_bytes/offset, next_offset that is neither null nor an integer, or text larger than maxBytes.

Common situations: Pages returned from a cache keyed by an older handle, a storage adapter returning excerpt data with original_page metadata, byte-offset paging logic off by one so the returned offset differs from the request, or oversized pages after a maxBytes configuration change.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/020b240d84be44e4. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/typescript/src/middleware/validate.ts:80

    reduction += r.tokens_before - r.tokens_after;
    if (r.unique_original) { unique += r.tokens_before - r.tokens_after; credited.add(r.original_sha256); }
  }
  for (const skipped of p.skipped) {
    if (!segments.has(skipped.segment_id) || seen.has(skipped.segment_id) || !isToken(skipped.reason)) invalid();
    seen.add(skipped.segment_id);
  }
  if (seen.size !== segments.size || m.tokens_before - m.tokens_after !== reduction || m.unique_tokens_reduced !== unique ||
    (p.replacements.length > 0 && (p.status !== 'optimized' || reduction <= m.recovery_overhead_tokens)) ||
    (p.status === 'optimized' && p.replacements.length === 0)) invalid();
  return p;
}

export async function validatePage(value: unknown, args: RetrieveArgs, maxBytes: number): Promise<RecoveryPage> {
  const p = value as RecoveryPage;
  if (!p || p.schema_version !== 1 || p.handle !== args.handle || !isToken(p.source_id) || !isHash(p.original_sha256) ||
    typeof p.text !== 'string' || byteLength(p.text) > maxBytes || !integer(p.total_bytes) || !integer(p.offset) || p.offset !== (args.offset ?? 0) ||
    !['original_page','excerpt'].includes(p.kind) || typeof p.complete !== 'boolean' ||
    !(p.next_offset === null || integer(p.next_offset))) throw new MiddlewareError('invalid_recovery');
  const length = byteLength(p.text);
  if (p.kind === 'original_page' && (p.offset + length > p.total_bytes ||
    p.next_offset !== (p.offset + length < p.total_bytes ? p.offset + length : null) ||
    p.complete !== (p.offset === 0 && length === p.total_bytes))) throw new MiddlewareError('invalid_recovery');
  if (p.kind === 'excerpt' && (p.complete || !args.query || p.next_offset !== 0)) throw new MiddlewareError('invalid_recovery');
  if (p.complete && await sha256(p.text) !== p.original_sha256) throw new MiddlewareError('invalid_recovery');
  return p;
}

View on GitHub (pinned to 3ee70a1026)