{"record":{"id":"2463be5d4b39ff5b","repo":"nexu-io/open-design","slug":"connector-execution-failed","errorCode":"CONNECTOR_EXECUTION_FAILED","errorMessage":"Composio OAuth state is missing or expired","messagePattern":"Composio OAuth state is missing or expired","errorType":"exception","errorClass":"ConnectorServiceError","httpStatus":400,"severity":"error","filePath":"apps/daemon/src/connectors/composio.ts","lineNumber":725,"sourceCode":"  cancelPendingConnections(connectorId: string): number {\n    this.pruneExpiredPendingConnections();\n    let cancelled = 0;\n    for (const [state, pending] of this.pendingConnections.entries()) {\n      if (pending.connectorId !== connectorId) continue;\n      this.pendingConnections.delete(state);\n      cancelled += 1;\n    }\n    return cancelled;\n  }\n\n  async completeConnection(input: { definition: ConnectorCatalogDefinition; state: string; providerConnectionId?: string; status?: string; signal?: AbortSignal }): Promise<ComposioConnectionCompletion> {\n    this.pruneExpiredPendingConnections();\n\n    const connectorId = input.definition.id;\n    const pending = this.pendingConnections.get(input.state);\n    this.pendingConnections.delete(input.state);\n    if (!pending || pending.connectorId !== connectorId || pending.expiresAtMs < Date.now()) {\n      throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth state is missing or expired', 400, { connectorId });\n    }\n    if (input.status && input.status.toLowerCase() !== 'success') {\n      throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth did not complete successfully', 400, { connectorId });\n    }\n    const providerConnectionId = input.providerConnectionId ?? pending.providerConnectionId;\n    if (input.providerConnectionId && pending.providerConnectionId && input.providerConnectionId !== pending.providerConnectionId) {\n      throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio callback connection id did not match pending connection', 403, { connectorId });\n    }\n    if (!providerConnectionId) {\n      throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio callback did not include a connection id', 400, { connectorId });\n    }\n    const expectedAuthConfigId = await this.getAuthConfigId(input.definition, input.signal);\n    const response = await this.getValidatedConnectedAccount(input.definition, providerConnectionId, expectedAuthConfigId, input.signal);\n    const authConfigId = getString(response.auth_config?.id);\n    if (authConfigId) this.storeAuthConfigId(input.definition, authConfigId, getString(response.toolkit?.slug) ?? input.definition.providerConnectorId);\n    return this.connectionToCredentials(input.definition, providerConnectionId, response);\n  }\n","sourceCodeStart":707,"sourceCodeEnd":743,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/connectors/composio.ts#L707-L743","documentation":"Thrown by ComposioConnector.completeConnection when the OAuth state token passed in the callback does not match any pending connection in the in-memory pendingConnections map, or the pending entry has expired, or it belongs to a different connector. The state token is a CSRF/OAuth-flow correlation token created during initiateConnection and stored with an expiry. This is a ConnectorServiceError with code CONNECTOR_EXECUTION_FAILED and HTTP 400, meaning the OAuth callback is invalid or arrived too late.","triggerScenarios":"The OAuth callback (redirect URL handler) calls completeConnection with a state token that: (a) was never created (forged or mismatched callback), (b) was already consumed by a previous callback (duplicate callback), (c) has expired (expiresAtMs < Date.now() after the OAuth flow took too long), or (d) was created for a different connectorId.","commonSituations":"User takes too long to complete the OAuth consent screen and the pending connection expires. The OAuth provider redirects twice (double-click). The state parameter was corrupted in transit. The user started OAuth for connector A but the callback is routed to connector B's handler.","solutions":["Restart the OAuth flow: call initiateConnection again to get a fresh state token, then redirect the user to re-authorize.","Ensure the callback URL is invoked only once per OAuth flow (deduplicate on the client side).","Check the pending connection TTL (expiresAtMs) and increase it if users consistently take longer than the window.","Verify the connectorId in the callback matches the one used to initiate the connection."],"exampleFix":"// before — calling completeConnection with a stale state\nawait connector.completeConnection({\n  definition,\n  state: oldStateToken,  // may be expired or already consumed\n});\n\n// after — restart the flow on failure\ntry {\n  await connector.completeConnection({ definition, state });\n} catch (error) {\n  if (error instanceof ConnectorServiceError &&\n      error.status === 400 &&\n      error.message.includes('state is missing or expired')) {\n    // Restart OAuth: get a new auth URL and redirect user\n    const { authUrl } = await connector.initiateConnection(definition);\n    return res.redirect(authUrl);\n  }\n  throw error;\n}","handlingStrategy":"validation","validationCode":"// Before calling completeConnection, verify the state token format\n// and check it hasn't already been consumed.\nfunction isValidStateToken(state: string | undefined): state is string {\n  return typeof state === 'string' && state.length >= 16;\n}\n\n// Validate before calling:\nif (!isValidStateToken(input.state)) {\n  return res.status(400).json({\n    code: 'CONNECTOR_EXECUTION_FAILED',\n    message: 'Invalid or missing OAuth state token.',\n  });\n}","typeGuard":"import { ConnectorServiceError } from './connectors/service.js';\n\nexport function isOAuthStateExpiredError(\n  error: unknown,\n): error is ConnectorServiceError {\n  return (\n    error instanceof ConnectorServiceError &&\n    error.code === 'CONNECTOR_EXECUTION_FAILED' &&\n    error.message.includes('state is missing or expired')\n  );\n}","tryCatchPattern":"try {\n  await connector.completeConnection({ definition, state, providerConnectionId, status });\n} catch (error) {\n  if (error instanceof ConnectorServiceError &&\n      error.code === 'CONNECTOR_EXECUTION_FAILED' &&\n      error.message.includes('state is missing or expired')) {\n    // Restart the OAuth flow with a fresh state token\n    const { authUrl } = await connector.initiateConnection(definition);\n    return res.redirect(authUrl);\n  }\n  throw error;\n}","preventionTips":["Start a fresh OAuth flow if the callback arrives after the pending connection TTL expires.","Ensure the callback URL is invoked only once — deduplicate on the client side.","Store the state token securely during initiateConnection and match it exactly in the callback.","Increase the pending connection TTL if users consistently take longer than the window."],"tags":["oauth","connectors","composio","authentication","state-management"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}