toeverything/AFFiNE · error · MissingOauthQueryParameter

missing_oauth_query_parameter

missing_oauth_query_parameter

Error message

Missing query parameter `${name}`.

What it means

Thrown by the POST /api/calendar/oauth/preflight endpoint when the request body omits the 'provider' field. The error name is misleading — the code reads @Body('provider'), not a query string — but it is a generic OAuth-preflight guard reused across the codebase (also thrown for missing 'code' and 'state' on the callback). It is a user-facing bad_request error (MissingOauthQueryParameter, code 'missing_oauth_query_parameter') carrying the missing field name so the client can render a targeted message.

Source

Thrown at packages/backend/server/src/plugins/calendar/controller.ts:42

import { CalendarService } from './service';

@Controller('/api/calendar')
export class CalendarController {
  constructor(
    private readonly calendar: CalendarService,
    private readonly oauth: CalendarOAuthService,
    private readonly url: URLHelper
  ) {}

  @Post('/oauth/preflight')
  @HttpCode(HttpStatus.OK)
  async preflight(
    @CurrentUser() user: CurrentUser,
    @Body('provider') providerName?: CalendarProviderName,
    @Body('redirect_uri') redirectUri?: string
  ) {
    if (!providerName) {
      throw new MissingOauthQueryParameter({ name: 'provider' });
    }

    if (!this.calendar.isProviderAvailableFor(providerName, { oauth: true })) {
      throw new UnknownOauthProvider({ name: providerName });
    }

    await this.calendar.assertCanLinkProvider(user.id, providerName);

    const state = await this.oauth.saveOAuthState({
      provider: providerName,
      userId: user.id,
      redirectUri,
    });

    const callbackUrl = this.calendar.getCallbackUrl();
    const authUrl = this.calendar.getAuthUrl(providerName, state, callbackUrl);

    return { url: authUrl };

View on GitHub (pinned to 26c515e050)

Solutions

  1. POST a JSON body containing { "provider": "google" } (or "caldav") with header Content-Type: application/json to /api/calendar/oauth/preflight.
  2. If the client is sending provider as a URL query string, move it into the JSON request body — the @Body decorator will not read @Query.
  3. Verify the request is actually reaching the controller and not a validation pipe that strips unknown fields; ensure no global ValidationPipe with whitelist:true is dropping provider because the DTO field name differs.
  4. In frontend code, guard the submit handler so the button is disabled until a provider is chosen, eliminating the empty-body case.

Example fix

// before
fetch('/api/calendar/oauth/preflight', {
  method: 'POST',
  body: JSON.stringify({ redirect_uri }),
});

// after
fetch('/api/calendar/oauth/preflight', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ provider: 'google', redirect_uri }),
});
Defensive patterns

Strategy: validation

Validate before calling

// Run before calling /api/calendar/oauth/preflight
function buildPreflightBody(input: { provider?: string; redirect_uri?: string }) {
  if (!input.provider || typeof input.provider !== 'string') {
    throw new Error('provider is required and must be a non-empty string');
  }
  return { provider: input.provider, redirect_uri: input.redirect_uri };
}

const body = buildPreflightBody(formData);
await fetch('/api/calendar/oauth/preflight', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});

Type guard

function isPreflightProvider(value: unknown): value is 'google' | 'caldav' {
  return value === 'google' || value === 'caldav';
}

Try / catch

try {
  await startCalendarOAuth('google');
} catch (err) {
  if (err instanceof Error && err.message.includes('missing_oauth_query_parameter') && err.message.includes('provider')) {
    showFieldError('provider', 'Please select a calendar provider.');
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: A POST to /api/calendar/oauth/preflight whose JSON body lacks a 'provider' key, sends it as null/empty string, or sends a content-type that NestJS cannot parse into the @Body('provider') decorator. The guard at controller.ts:41 (`if (!providerName)`) fires before any provider lookup or state persistence, so the request fails fast with HTTP 400.

Common situations: Frontend form submitting before a provider is selected; a client built against an older API that sent provider as a query param (?provider=google) instead of in the body; a malformed fetch with the wrong Content-Type (e.g. text/plain) so Nest's body parser leaves provider undefined; automated tests that construct the preflight request without the field.

Related errors


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