toeverything/AFFiNE · warning · BadRequest

bad_request

bad_request

Error message

pagination.after and pagination.offset cannot be used together

What it means

A BadRequest error (HTTP 400) thrown by assertPaginationInput() when a GraphQL pagination input provides both a cursor ('after') and a numeric offset simultaneously. Cursor-based and offset-based pagination are mutually exclusive strategies; combining them is ambiguous. The check runs inside the PaginationInput.decode pipe transform, so it fires before the resolver executes.

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 26c515e050)

Solutions

  1. Use only cursor-based pagination (set 'after', omit 'offset' or keep it at 0).
  2. Or use only offset-based pagination (set 'offset', omit 'after').
  3. Review client-side pagination logic to ensure it doesn't send both parameters together.

Example fix

// before (GraphQL variables)
{ "input": { "first": 10, "after": "Y3Vyc29y", "offset": 5 } } // throws

// after (cursor-based)
{ "input": { "first": 10, "after": "Y3Vyc29y" } }
// or (offset-based)
{ "input": { "first": 10, "offset": 5 } }
Defensive patterns

Strategy: validation

Validate before calling

function validatePaginationInput(input?: PaginationInput) {
  if (input?.after && (input.offset ?? 0) > 0) {
    throw new Error('Use either cursor (after) or offset, not both.');
  }
}
validatePaginationInput(paginationInput);

Type guard

const isExclusivePagination = (
  input?: PaginationInput
): boolean => !(input?.after && (input.offset ?? 0) > 0);

Try / catch

try {
  const result = await client.query({
    query: MY_QUERY,
    variables: { input: paginationInput },
  });
} catch (e) {
  if (e.message?.includes('pagination.after and pagination.offset')) {
    // remove offset or after and retry
    const fixed = { ...paginationInput, offset: 0 };
    await client.query({ query: MY_QUERY, variables: { input: fixed } });
  }
}

Prevention

When it happens

Trigger: Sending a GraphQL query with pagination input containing both 'after' (a base64 cursor) and 'offset > 0'. For example: { first: 10, after: 'Y3Vyc29y', offset: 5 }. The PaginationInput.decode pipe transform calls assertPaginationInput which rejects this combination.

Common situations: Client code migrating from offset-based to cursor-based pagination that forgot to remove the offset field. Frontend pagination components that send all available parameters defensively. API client libraries that auto-populate offset with a non-zero default.

Related errors


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