toeverything/AFFiNE · error · BadRequest

bad_request

bad_request

Error message

pagination.after and pagination.offset cannot be used together

What it means

assertPaginationInput rejects GraphQL pagination input that combines a cursor (after) with offset > 0 by throwing a BadRequest (code bad_request). AFFiNE list queries support either cursor-based paging (first/after) or offset-based paging (first/offset), never both at once — mixing them makes the requested window ambiguous.

Solutions

  1. Remove offset whenever after is set — use { first, after } only.
  2. Clear after whenever you set offset.
  3. Build pagination input through a single helper that enforces one mode at a time.

Example fix

# before
pagination: { first: 10, after: cursor, offset: 20 }

# after
pagination: { first: 10, after: cursor }  # offset removed; cursor and offset are mutually exclusive
Defensive patterns

Strategy: validation

Validate before calling

function buildPagination(mode: 'cursor' | 'offset', args: { first: number; cursor?: string; offset?: number }) {
  if (mode === 'cursor') return { first: args.first, after: args.cursor };
  return { first: args.first, offset: args.offset ?? 0 };
}
// never emits both `after` and a positive `offset`

Type guard

const isSingleModePagination = (p?: PaginationInput): boolean =>
  !p || !(p.after && p.offset > 0);

Try / catch

try {
  const result = await client.request(query, { pagination });
} catch (e) {
  if (e instanceof Error && e.message.includes('after and pagination.offset cannot be used together')) {
    delete pagination.offset; // fall back to cursor-only paging and retry
    return client.request(query, { pagination });
  } else throw e;
}

Prevention

When it happens

Trigger: A query with pagination: { first: 10, after: '<cursor>', offset: 20 }; a client that keeps the cursor from the previous page while also appending an offset; UI code switching pagination modes without clearing the other parameter.

Common situations: Relay-style clients adding an offset fallback; pagination components that merge state from two modes; scripts generated from query builders that include every optional field.

Understand the failure class

Background: BAD_REQUEST error code: request rejected as invalid (HTTP 400) - causes and fixes across libraries — this error's family across 8 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/a503b22a0e56e93d. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/base/graphql/pagination.ts:66

  })
  after?: string | null;

  // NOT IMPLEMENTED YET
  // @Field(() => String, {
  //   nullable: true,
  //   description:
  //     'returns the elements in the list that come before the specified cursor.',
  // })
  // before?: string | null;
}

export function assertPaginationInput(paginationInput?: PaginationInput) {
  if (!paginationInput) {
    return;
  }

  if (paginationInput.after && paginationInput.offset > 0) {
    throw new BadRequest(
      'pagination.after and pagination.offset cannot be used together'
    );
  }
}

const encode = (input: unknown) => {
  let inputStr: string;
  if (input instanceof Date) {
    inputStr = input.toISOString();
  } else if (typeof input === 'string') {
    inputStr = input;
  } else {
    inputStr = String(input);
  }
  return Buffer.from(inputStr).toString('base64');
};
const decode = (base64String?: string | null) =>
  base64String ? Buffer.from(base64String, 'base64').toString('utf-8') : null;

View on GitHub (pinned to b4c8548c09)