nexu-io/open-design · error · ConnectorServiceError
CONNECTOR_EXECUTION_FAILED
CONNECTOR_EXECUTION_FAILED
Error message
Composio OAuth state is missing or expired
What it means
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.
Source
Thrown at apps/daemon/src/connectors/composio.ts:725
cancelPendingConnections(connectorId: string): number {
this.pruneExpiredPendingConnections();
let cancelled = 0;
for (const [state, pending] of this.pendingConnections.entries()) {
if (pending.connectorId !== connectorId) continue;
this.pendingConnections.delete(state);
cancelled += 1;
}
return cancelled;
}
async completeConnection(input: { definition: ConnectorCatalogDefinition; state: string; providerConnectionId?: string; status?: string; signal?: AbortSignal }): Promise<ComposioConnectionCompletion> {
this.pruneExpiredPendingConnections();
const connectorId = input.definition.id;
const pending = this.pendingConnections.get(input.state);
this.pendingConnections.delete(input.state);
if (!pending || pending.connectorId !== connectorId || pending.expiresAtMs < Date.now()) {
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth state is missing or expired', 400, { connectorId });
}
if (input.status && input.status.toLowerCase() !== 'success') {
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth did not complete successfully', 400, { connectorId });
}
const providerConnectionId = input.providerConnectionId ?? pending.providerConnectionId;
if (input.providerConnectionId && pending.providerConnectionId && input.providerConnectionId !== pending.providerConnectionId) {
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio callback connection id did not match pending connection', 403, { connectorId });
}
if (!providerConnectionId) {
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio callback did not include a connection id', 400, { connectorId });
}
const expectedAuthConfigId = await this.getAuthConfigId(input.definition, input.signal);
const response = await this.getValidatedConnectedAccount(input.definition, providerConnectionId, expectedAuthConfigId, input.signal);
const authConfigId = getString(response.auth_config?.id);
if (authConfigId) this.storeAuthConfigId(input.definition, authConfigId, getString(response.toolkit?.slug) ?? input.definition.providerConnectorId);
return this.connectionToCredentials(input.definition, providerConnectionId, response);
}
View on GitHub (pinned to 5be4028344)
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.
Example fix
// before — calling completeConnection with a stale state
await connector.completeConnection({
definition,
state: oldStateToken, // may be expired or already consumed
});
// after — restart the flow on failure
try {
await connector.completeConnection({ definition, state });
} catch (error) {
if (error instanceof ConnectorServiceError &&
error.status === 400 &&
error.message.includes('state is missing or expired')) {
// Restart OAuth: get a new auth URL and redirect user
const { authUrl } = await connector.initiateConnection(definition);
return res.redirect(authUrl);
}
throw error;
} Defensive patterns
Strategy: validation
Validate before calling
// Before calling completeConnection, verify the state token format
// and check it hasn't already been consumed.
function isValidStateToken(state: string | undefined): state is string {
return typeof state === 'string' && state.length >= 16;
}
// Validate before calling:
if (!isValidStateToken(input.state)) {
return res.status(400).json({
code: 'CONNECTOR_EXECUTION_FAILED',
message: 'Invalid or missing OAuth state token.',
});
} Type guard
import { ConnectorServiceError } from './connectors/service.js';
export function isOAuthStateExpiredError(
error: unknown,
): error is ConnectorServiceError {
return (
error instanceof ConnectorServiceError &&
error.code === 'CONNECTOR_EXECUTION_FAILED' &&
error.message.includes('state is missing or expired')
);
} Try / catch
try {
await connector.completeConnection({ definition, state, providerConnectionId, status });
} catch (error) {
if (error instanceof ConnectorServiceError &&
error.code === 'CONNECTOR_EXECUTION_FAILED' &&
error.message.includes('state is missing or expired')) {
// Restart the OAuth flow with a fresh state token
const { authUrl } = await connector.initiateConnection(definition);
return res.redirect(authUrl);
}
throw error;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- CONNECTOR_EXECUTION_FAILED
- CONNECTOR_OUTPUT_TOO_LARGE
- CONNECTOR_NOT_FOUND
- CONNECTOR_NOT_CONNECTED
- xAI OAuth state not found or expired
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/2463be5d4b39ff5b.
Report an issue: GitHub.