{"record":{"id":"773133a650a500ac","repo":"toeverything/AFFiNE","slug":"missing-oauth-query-parameter","errorCode":"missing_oauth_query_parameter","errorMessage":"Missing query parameter `${name}`.","messagePattern":"Missing query parameter `(.+?)`\\.","errorType":"exception","errorClass":"MissingOauthQueryParameter","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/calendar/controller.ts","lineNumber":42,"sourceCode":"import { CalendarService } from './service';\n\n@Controller('/api/calendar')\nexport class CalendarController {\n  constructor(\n    private readonly calendar: CalendarService,\n    private readonly oauth: CalendarOAuthService,\n    private readonly url: URLHelper\n  ) {}\n\n  @Post('/oauth/preflight')\n  @HttpCode(HttpStatus.OK)\n  async preflight(\n    @CurrentUser() user: CurrentUser,\n    @Body('provider') providerName?: CalendarProviderName,\n    @Body('redirect_uri') redirectUri?: string\n  ) {\n    if (!providerName) {\n      throw new MissingOauthQueryParameter({ name: 'provider' });\n    }\n\n    if (!this.calendar.isProviderAvailableFor(providerName, { oauth: true })) {\n      throw new UnknownOauthProvider({ name: providerName });\n    }\n\n    await this.calendar.assertCanLinkProvider(user.id, providerName);\n\n    const state = await this.oauth.saveOAuthState({\n      provider: providerName,\n      userId: user.id,\n      redirectUri,\n    });\n\n    const callbackUrl = this.calendar.getCallbackUrl();\n    const authUrl = this.calendar.getAuthUrl(providerName, state, callbackUrl);\n\n    return { url: authUrl };","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/plugins/calendar/controller.ts#L24-L60","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["POST a JSON body containing { \"provider\": \"google\" } (or \"caldav\") with header Content-Type: application/json to /api/calendar/oauth/preflight.","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.","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.","In frontend code, guard the submit handler so the button is disabled until a provider is chosen, eliminating the empty-body case."],"exampleFix":"// before\nfetch('/api/calendar/oauth/preflight', {\n  method: 'POST',\n  body: JSON.stringify({ redirect_uri }),\n});\n\n// after\nfetch('/api/calendar/oauth/preflight', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ provider: 'google', redirect_uri }),\n});","handlingStrategy":"validation","validationCode":"// Run before calling /api/calendar/oauth/preflight\nfunction buildPreflightBody(input: { provider?: string; redirect_uri?: string }) {\n  if (!input.provider || typeof input.provider !== 'string') {\n    throw new Error('provider is required and must be a non-empty string');\n  }\n  return { provider: input.provider, redirect_uri: input.redirect_uri };\n}\n\nconst body = buildPreflightBody(formData);\nawait fetch('/api/calendar/oauth/preflight', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify(body),\n});","typeGuard":"function isPreflightProvider(value: unknown): value is 'google' | 'caldav' {\n  return value === 'google' || value === 'caldav';\n}","tryCatchPattern":"try {\n  await startCalendarOAuth('google');\n} catch (err) {\n  if (err instanceof Error && err.message.includes('missing_oauth_query_parameter') && err.message.includes('provider')) {\n    showFieldError('provider', 'Please select a calendar provider.');\n  } else {\n    throw err;\n  }\n}","preventionTips":["Always set Content-Type: application/json on POST bodies so Nest's parser populates @Body fields.","Disable the submit button until a provider is selected in the UI.","Keep the provider option list in sync with the CalendarProviderName enum so the client can never send an unexpected value.","Add a client-side schema check (e.g. zod) for the preflight request shape before sending."],"tags":["oauth","calendar","nestjs","typescript","validation","http-400"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}