toeverything/AFFiNE · error · MissingOauthQueryParameter
missing_oauth_query_parameter
missing_oauth_query_parameter
Error message
Missing query parameter `client_nonce`.
What it means
POST /oauth/preflight validates its JSON body with OAuthPreflightBodySchema (zod, strict). When the `client_nonce` field fails validation (missing, empty string, longer than 512 chars, or not a string), the server maps it to MissingOauthQueryParameter with name 'client_nonce'. Despite the legacy 'query parameter' wording, this is a request-body field required as an anti-replay nonce for the OAuth preflight.
Source
Thrown at packages/backend/server/src/plugins/oauth/controller.ts:47
@Controller('/api/oauth')
export class OAuthController {
constructor(
private readonly sessionIssuer: SessionIssuer,
private readonly oauth: OAuthService,
private readonly providerFactory: OAuthProviderFactory,
private readonly url: URLHelper
) {}
@Public()
@UseNamedGuard('version')
@Post('/preflight')
@HttpCode(HttpStatus.OK)
async preflight(@Req() req: Request, @Body() body?: unknown) {
const input = OAuthPreflightBodySchema.safeParse(body);
if (!input.success) {
const fields = new Set(input.error.issues.map(issue => issue.path[0]));
if (fields.has('client_nonce')) {
throw new MissingOauthQueryParameter({ name: 'client_nonce' });
}
if (fields.has('client')) {
throw new ActionForbidden();
}
if (fields.has('provider')) {
const provider =
body && typeof body === 'object' && 'provider' in body
? String(body.provider)
: '';
throw new UnknownOauthProvider({ name: provider });
}
throw new MissingOauthQueryParameter({ name: 'provider' });
}
const {
provider: unknownProviderName,
redirect_uri: redirectUri,
client,View on GitHub (pinned to 591f874dad)
Solutions
- Include client_nonce in the preflight body: a fresh random string, 1-512 chars (crypto.randomUUID() is ideal), regenerated for every login attempt
- Upgrade the client/frontend to a version matching the server's OAuth preflight contract
- If writing a custom client, mirror the schema exactly: { provider, client, redirect_uri?, client_nonce } with no extra keys (schema is strict)
Example fix
// before
await fetch('/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'Google', client: 'web' }) });
// after
await fetch('/oauth/preflight', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: 'Google', client: 'web', client_nonce: crypto.randomUUID() }),
}); Defensive patterns
Strategy: validation
Validate before calling
const PreflightBody = z.object({
provider: z.enum(['Google', 'GitHub', 'Apple', 'OIDC']),
redirect_uri: z.string().min(1).max(2048).nullish(),
client: z.enum(['web', 'affine', 'affine-canary', 'affine-beta', 'affine-dev']),
client_nonce: z.string().min(1).max(512),
});
// reject before the request
const body = PreflightBody.parse({ provider, client, redirect_uri, client_nonce: crypto.randomUUID() });
await post('/oauth/preflight', body); Type guard
function isPreflightBodyOk(b: unknown): b is { provider: string; client: string; client_nonce: string } {
return (
typeof b === 'object' && b !== null &&
typeof (b as any).client_nonce === 'string' && (b as any).client_nonce.length >= 1 && (b as any).client_nonce.length <= 512
);
} Try / catch
try { await post('/oauth/preflight', body); } catch (e) {
if ((e as any).code === 'missing_oauth_query_parameter' && (e as any).args?.name === 'client_nonce') {
body.client_nonce = crypto.randomUUID(); // regenerate and retry once
return post('/oauth/preflight', body);
}
throw e;
} Prevention
- Generate client_nonce fresh (crypto.randomUUID) for every preflight call — never cache or reuse it
- Keep a shared zod schema for the preflight body in the client and server codebases so contracts can't drift
- Send Content-Type: application/json; the schema expects a JSON body, not query strings
When it happens
Trigger: POST /oauth/preflight with body lacking client_nonce, client_nonce: '', a >512-char nonce, or a non-string nonce. Custom scripts/curl calls or older client builds that predate the client_nonce requirement hit this.
Common situations: Server upgraded to a version requiring per-attempt client_nonce while an older AFFiNE client or custom integration still posts {provider, client, redirect_uri}; curl testing without the field; nonce generator returning undefined after a refactor.
Related errors
- unknown_oauth_provider
- action_forbidden
- invalid_indexer_input
- ValueNotExists
- Param not a number: ${mode},${numbeToken.text}
AI-assisted analysis of toeverything/AFFiNE@591f874dad (2026-08-18).
Data as JSON: /api/errors/9b63a33a20e1b59f.
Report an issue: GitHub.