Skip to content

CONNECT_RETRY_HREF drops workspaceId, so retrying a failed connect runs the first-workspace path and reports "Workspace limit reached" for a lost query parameter #1159

Description

@rawdaymx

Summary

CONNECT_RETRY_HREF is the bare string "/channels/create" (features/channel-connect/lib/registry.ts:23), used by the four "try again" links a failed connect can show. It carries no workspaceId.

That matters because /channels/create treats the absence of workspaceId as "this user has no workspace yet". The page threads workspaceId into each channel's next hop only when it has one (create/page.tsx:127), the connect route reads it back as undefined (channels/create/messenger/route.ts:34), and both that route (:65-67) and the shared OAuth callback (integrations/[...integration]/callback.ts:321-323) then call createFirstWorkspace(userId) — whose own comment states the premise it is never given a chance to check:

// apps/builder/src/lib/workspace/create-first-workspace.ts:49-54
/**
 * First-channel path: the user has no workspace yet, so one is created before
 * the channel connects. …
 */

So an operator who was working inside a workspace, hit a connect failure, and clicked "try again" is routed down the first-workspace path. On the Community edition, where COMMUNITY_MAX_WORKSPACES = 1 (packages/business/src/workspace/service.ts:44), workspaceService.create then throws workspaceLimitReached, createFirstWorkspace converts it to redirect("/channels/create?error=workspaceLimitReached"), and the operator is shown "Workspace limit reached." The real cause was a dropped query parameter.

Environment

Measured against upstream/main @ be7234b93bb47936e9469016ccb82e9b2aa765b7 for the code
Runtime measurements a Codespace running this repository, edition community (NEXT_PUBLIC_EDITION=community), its own Postgres, browser driven with Playwright
Files identical the files below are byte-identical between 96032013e and be7234b93
Date 2026-09-12

Measured

1 · The retry target carries nothing. Four call sites, one literal:

$ git grep -n 'CONNECT_RETRY_HREF' be7234b93 -- apps/builder/src
…/channel-connect/lib/registry.ts:23:export const CONNECT_RETRY_HREF = "/channels/create"
…/channel-connect/components/connect-picker-screen.tsx:171:            href={CONNECT_RETRY_HREF}
…/channel-connect/components/connect-session-error-alert.tsx:39:          href={CONNECT_RETRY_HREF}
…/channel-connect/components/connecting-step.tsx:114:              href={CONNECT_RETRY_HREF}
…/integration-messenger/components/messenger-pages.tsx:79:                href={CONNECT_RETRY_HREF}

We could not get one of those four to render naturally — that needs a real failed provider session, and this environment has no Meta credentials. So the live half below starts from /channels/create directly, which is where those links land.

2 · Nothing re-attaches it downstream. Signed in as a user who already owns workspaces, GET /channels/create with no query, then clicking "Continue" on Messenger. Worth noting for anyone reading the code: the channel card is a <button type="button">, not a link (inbox-select-card.tsx:131-138), so there is no href to inspect — the navigation happens in handleInboxSelect. The resulting URL:

/channels/create?channel=messenger

No workspaceId, exactly as create/page.tsx:127's ternary predicts. From there the next hop is redirect("/channels/create/messenger"), still without it, and that route resolves workspaceId to undefined and calls createFirstWorkspace.

3 · The gate, against the real service and the real database. Rather than infer what happens at the end of that path, we called the same service createFirstWorkspace calls, for a user who already owns workspaces, on the Codespace's own database:

{
  "edicion": "community",
  "workspacesPropiosAntes": 3,
  "resultado": {
    "tipo": "ChatbotXException",
    "code": "workspaceLimitReached",
    "message": "Workspace limit reached for this plan"
  },
  "workspacesPropiosDespues": 3
}

It throws, and creates nothing. createFirstWorkspace catches exactly this code (:39-46) and redirects to ?error=workspaceLimitReached.

(That account happened to own three workspaces rather than one — the seed had inserted them directly. It does not change the gate, which fires on owned >= COMMUNITY_MAX_WORKSPACES, so it fires the same way at one.)

4 · What the operator ends up reading. GET /channels/create?error=workspaceLimitReached renders a destructive banner reading "Workspace limit reached." above the channel list, with every channel still selectable underneath. Nothing on the screen connects it to the workspace they were in, and nothing suggests retrying from inside that workspace would work.

Why we think this is a defect rather than the intended design

Creating a workspace when state carries no workspaceId is deliberate, and we are aware it has history: #786 added a dedupe lock around it and #792 removed that lock two hours later, deliberately, described as a simplification. We are not asking for #786 back, and we are not claiming the first-channel path is wrong.

What we think is unintended is that the premise is never established. createFirstWorkspace's comment asserts "the user has no workspace yet"; the two callers reach it purely from the absence of a query parameter, and this repository contains a supported, in-product way for that parameter to go missing for a user who does have one — the retry link. The function is correct about what it does and wrong about when it is called.

The Community consequence is what makes it worth your time: the operator does not get a stray workspace, they get an error attributing their failure to their plan. That is a support ticket that cannot be diagnosed from the message.

Suggested fix

Two independent halves; either alone is an improvement.

  1. Carry the context through the retry. CONNECT_RETRY_HREF is already the one place this route is spelled, which makes it the natural seam — make it a function of the workspace the operator was in (and, where the connect flow already knows it, the flow they came from), rather than a constant. All four call sites are inside components that have the workspace in scope.

  2. Establish the premise before acting on it. Have createFirstWorkspace check whether the user already owns a workspace and, if so, use it instead of creating — or at minimum fail with a code that says what actually happened rather than borrowing workspaceLimitReached. As it stands the quota error is doing double duty as "you are over your plan" and "we lost track of your workspace", and only one of those is the operator's problem.

We have not sent a patch for either. The first needs a decision about what the retry should carry, and the second about whether reusing an existing workspace is the behaviour you want on that path — both yours. Happy to implement whichever shape you pick.

What we did not verify

  • We never completed a real OAuth round trip. There is no Meta app on our side, so the middle of the path — retry link → provider consent → callback → createFirstWorkspace — is read from the code, not observed. What we measured is the two ends: the URL after choosing a channel with no workspaceId, and the service's behaviour for a user who already owns one.
  • We did not see any of the four retry links render. They need a failed provider session. The code is unambiguous about the href, but we did not watch an operator click one.
  • We did not test the non-Community path, where the create would presumably succeed and leave a stray "New Workspace" instead of an error. That is the behaviour we would expect from the same code, but we did not run it.
  • We did not check whether ConnectSessionErrorAlert's retry is reachable in a state where the workspace is genuinely unknown, in which case a bare /channels/create would be the right target for that one call site specifically.

Related issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions