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
- Use only cursor-based pagination (set 'after', omit 'offset' or keep it at 0).
- Or use only offset-based pagination (set 'offset', omit 'after').
- 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
- Decide on one pagination strategy (cursor or offset) and don't mix them.
- In client code, only populate the fields relevant to the chosen strategy.
- Add client-side validation that warns before sending both parameters.
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
- action_forbidden
- Invalid config for module [${module}] with key [${key}] Valu
- Invalid duration string unit ${ch}
- email_token_not_found
- same_email_provided
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/a503b22a0e56e93d.
Report an issue: GitHub.