Skip to content

Showcase REST connectors hard-wire baseUrl to http://127.0.0.1:3000, so self-ping flows fail fetch failed on any isolated instance — a port mismatch, not an egress block #7538

Description

@huangyiirene

Symptom

The showcase's REST connectors — StatusApi and StatusOpenApi — declare baseUrl as the literal http://127.0.0.1:3000 in their providerConfig. The value is not env-overridable.

Any instance that does not happen to be listening on port 3000 therefore cannot self-ping: every flow that dispatches through these connectors fails with fetch failed. That is the normal case for CI, for QA runs, and for any local dev that boots on an isolated port — which is exactly the configuration these fixtures most need to work in.

The important part: this is a port mismatch, not the sandbox egress block. Those two failure modes present identically (fetch failed) and it is easy — and wrong — to write the failure off as "no outbound network in the sandbox" and stop. The run established causality: putting a throwaway TCP forwarder from 3000 to the instance's real port made the same flows succeed with no other change, giving clean before/after evidence that the destination address is the whole problem.

The cost is not just a red run. It teaches every reader of the showcase that connector baseUrl is a hard-coded literal, and it silently converts a fixture defect into an apparent platform/environment defect for anyone who hits it without the forwarder trick.

Root cause

Fixture authoring, not the connector engine — dispatch itself is proven working in the same run (the MCP variant dispatches and captures output end to end, and an unregistered connector gives a named refusal rather than a silent no-op).

The providerConfig for StatusApi and StatusOpenApi carries baseUrl: 'http://127.0.0.1:3000' as a literal, with no env indirection, so the value cannot follow the port the instance actually bound.

Two acceptable resolutions:

  1. Make baseUrl env-overridable (read the instance's real base URL / port from the environment, defaulting to the current literal) so the self-ping fixtures work wherever the app boots. This is the better outcome — the flows become genuinely runnable and the fixture stops teaching a hard-coded address.
  2. Failing that, add a knownGaps entry so the failure is declared rather than rediscovered. This is strictly the fallback: it makes the gap honest but leaves the clauses unrunnable.

Reproduction

  1. Boot the showcase on an isolated port (anything other than 3000), with an isolated file DB.
  2. Trigger a flow that dispatches through StatusApi or StatusOpenApi.
  3. Observe the run fails with fetch failed.
  4. Confirm it is the address and not egress: start a TCP forwarder listening on 127.0.0.1:3000 and forwarding to the instance's real port, then re-trigger the same flow. It succeeds, unchanged.

Re-check the literals:

rg -n "127.0.0.1:3000|baseUrl" examples --glob '*onnector*'
rg -rn "StatusApi|StatusOpenApi" examples

Source

Extracted from the QA run #7516 (framework a86db17). Listed there as item 1 under "Fixture / authoring issues to fix"; it partially blocks connector-dispatch-matrix.

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings triage — resolved the filing-time pm:queue+finding dual state to pm:queue (finding removed).

    • Grade rationale: concrete fixture defect with a causal repro (TCP-forwarder before/after isolates the address as the whole problem), named literals, and a stated preferred fix. Dispatchable as filed.
    • Premise verified on origin/main @ 744b8f5: examples/app-showcase/src/system/connectors/index.ts:57 and :89 still carry the literal baseUrl: 'http://127.0.0.1:3000' (plus status-openapi.json:8's servers URL). Bonus for the claiming seat: examples/app-showcase/objectstack.config.ts:135 already does process.env.SHOWCASE_SELF_URL ?? 'http://127.0.0.1:3000' — the env-indirection pattern resolution 1 wants exists in-repo, one file away; consider reusing the same variable so the two self-URL sources cannot diverge.
    • Routing check: connector fixtures exercise packages/connectors/* surface ⇒ domain:services per the domain table. Kept.
    • Dedup: Showcase flow comments overstate behaviour: TaskCompletedRestPingFlow / ShowcaseDeclarativeConnectorPingFlow claim the connector response is captured on the run, but neither declares an isOutput variable #7542 (isOutput authoring gap) is the companion from the same QA run — different files, different fix. No overlap.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim: PM loop, services lane, wave 2 round 3
    Session: session_015fkdTyGmMD5s8ZtEifvuGy
    Branch: claude/issue-7538-showcase-connector-baseurl
    Worktree: objectstack-issue-7538
    Domain: domain:services
    File surface: examples/app-showcase/src/system/connectors/index.ts (:57/:89 baseUrl literals), examples/app-showcase/src/system/connectors/status-openapi.json (:8 servers URL), and — if the env-indirection route is taken — a read of the pattern at examples/app-showcase/objectstack.config.ts:135 (SHOWCASE_SELF_URL). Stop on breach.
    Container & model: S-grade fixture repair with triage-verified premise and a named preferred fix, mode:subagent, model: opus.
    Serial constraints cleared: ⚠️ batch-mate #7542 is in the SAME example app (examples/app-showcase) — fix sites are disjoint per triage's own dedup (src/system/connectors/* here vs src/automation/flows/index.ts there; "different files, different fix... no overlap"). Region-level declaration is this line; both devs merge origin/main before opening their PR, and any conflict goes to the merge queue's arbitration, not manual ordering. No open PR or other in-flight claim touches examples/app-showcase.


    Generated by Claude Code

  4. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7621, reviewed against GitHub rather than the report's own claims.

    Verified in the diff (5 files, all inside the declared surface — src/automation/flows/ untouched, batch-mate #7542's face respected):

    • A false in-code assertion was measured and removed, not worked around. The old comment claimed "metadata files don't read env"; the dev proved it false (connectors/index.ts is an ordinary Node module in objectstack.config.ts's import graph, which already reads process.env) and documented the real caveat in its place: env is read wherever the config loads, so os dev/os serve follows the live environment while an os build artifact freezes it at build time. Folklore replaced by a mechanism statement.
    • Triage's bonus lead executed exactly: one shared resolveShowcaseSelfUrl() now feeds both self-URL sources — the declarative connector instances and the ConnectorRestPlugin line that previously inlined its own fallback — so the two cannot diverge again.
    • The resolution order follows the CLI's own inputs: SHOWCASE_SELF_URL → OS_PORT → deprecated PORT → the historical literal — the same names, same order, as serve.ts's port pick. An isolated-port boot self-pings with zero extra configuration, which is precisely the issue's repro scenario.
    • status-openapi.json kept its literal for a measured reason: a static document cannot follow a bound port, and createOpenApiConnector resolves config.baseUrl ?? document.servers[0].url, so the env-resolved connector value always wins; the doc's description now says so.
    • Reverse verification makes the right structural point: reverting the literals reds exactly the four environment-following cases while the defaults-to-3000 cases stay green — a hard-wired literal is by construction correct in the default case, so default-case assertions alone can never catch this class. Both halves pinned.
    • End-to-end through the real CLI compile (OS_PORT=4711 → both connectors' compiled baseUrl follow; no env → 3000), plus a live self-ping probe whose counterfactual reproduces the issue's exact ECONNREFUSED 127.0.0.1:3000.
    • Coverage-gap note worth keeping: showcase-smoke.yml runs Playwright at a hard-coded PORT = 3000 and drives no connector dispatch — it exercises exactly the one port where this bug is invisible. Recorded here for whoever next trusts that smoke as connector coverage.
    • Suites: example-showcase 19 files / 183 tests green, typecheck clean.
    • Changeset route: examples/app-showcase is private: true ⇒ skip-changeset label; applied at this review with read-back.

    CI converging. Driving to landing: gate conclusions → ready → arm → queue verify.


    Generated by Claude Code

  5. os-help commented on Aug 11, 2026

    @os-help
    Collaborator
    {
      "issue": 7538,
      "status": "done",
      "branch": "claude/issue-7538-showcase-connector-baseurl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7621",
      "premise_still_valid": true,
      "summary": "Premise confirmed on origin/main: examples/app-showcase/src/system/connectors/index.ts:57 and :89 carried the literal baseUrl 'http://127.0.0.1:3000' (plus status-openapi.json:8's servers URL). Took the issue's preferred resolution 1 (env-overridable), in the shape that keeps ONE source of truth: a new examples/app-showcase/src/system/self-url.ts exports resolveShowcaseSelfUrl(), and BOTH self-URL sources now call it — the two declarative connector instances and the ConnectorRestPlugin at objectstack.config.ts:135, which previously inlined its own process.env read. Precedence: SHOWCASE_SELF_URL, else OS_PORT then its deprecated alias PORT (the same names in the same order the CLI itself reads to pick the listen port, serve.ts:204), else the unchanged 'http://127.0.0.1:3000' default. Falsified the PM hypothesis's premise-in-the-code: the old comment at :55 claimed 'metadata files don't read env' — that is FALSE (connectors/index.ts is an ordinary Node module imported by objectstack.config.ts:37, same process, same evaluation), so no config-layer injection was needed; the comment is removed. The real caveat, now documented rather than folklore: on the os dev/serve path serve loads the config so the value follows the live environment, while on the artifact-only path (os build → os start --artifact) connector metadata is serialized into dist/objectstack.json and the value is frozen at BUILD time. status-openapi.json keeps its literal servers[0].url because a static document cannot follow a bound port — createOpenApiConnector resolves config.baseUrl ?? document.servers[0].url (openapi-connector.ts:152), so the connector's env-resolved baseUrl always wins; its info.description now says so.",
      "tests": "pnpm --filter @objectstack/example-showcase^... build (dependency closure, exit 0) → typecheck: 'tsc --noEmit' clean → test: 'Test Files 19 passed (19) / Tests 183 passed (183)' post-merge with origin/main. New test examples/app-showcase/test/connector-self-url.test.ts (12 cases). REVERSE VERIFICATION, predicted direction RED-on-revert, confirmed: restoring the two literals via 'git checkout origin/main -- .../connectors/index.ts' gave 'Tests 4 failed | 8 passed (12)' — the 4 environment-following cases red ('AssertionError: expected http://127.0.0.1:3000 to be http://127.0.0.1:4711'), while every 'still defaults to 3000' case stayed GREEN, which is the point: a literal is by construction still correct in the default case, so default-case assertions alone can never detect this bug. END-TO-END through the real CLI compile: OS_PORT=4711 → [[showcase_status_api,http://127.0.0.1:4711],[showcase_status_openapi,http://127.0.0.1:4711]]; no env → both http://127.0.0.1:3000. LIVE SELF-PING via a throwaway probe (server on 4711, dispatch through the real createRestConnector/createOpenApiConnector built from the compiled providerConfig): both returned {status:ok,servedOnPort:4711}, server hits ['GET /api/v1/health','GET /api/v1/health']. COUNTERFACTUAL against the same 4711 server with the pre-fix literal reproduced the issue's exact symptom: 'TypeError: fetch failed / [cause]: Error: connect ECONNREFUSED 127.0.0.1:3000' — the same before/after causality the QA run needed a TCP forwarder for. Gate: node scripts/check-nul-bytes.mjs OK (7057 files); control-byte self-scan of all 5 touched files clean. CI at report time (card directs reporting at draft-PR time, not idle-polling): Check Changeset SUCCESS, Check PR Size / Auto Label / ADR gate / Console Pin Freshness / Build Core / Dogfood Verify CLI / 'No other open PR may claim the same issue' all success; ESLint and TypeScript Type Check still in_progress; Test Core (1-3/3), Dogfood Regression Gate (1-3/3) and Temporal Conformance still in_progress. No gate red at report time.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes the JSON cannot carry:

    • skip-changeset route judged correct, and the label is already applied. examples/app-showcase is "private": true and publishes nothing, so no user-visible package releases and no .changeset/*.md is owed. Labels read back after the bots settled: tests, skip-changeset — the label landed ahead of Check Changeset's re-read and that job concluded success, so the PM does not need to apply it at review.
    • showcase-smoke.yml does NOT cover this, as the card asked me to name. It is a Playwright console nav-render smoke — non-blocking, workflow_dispatch + nightly only — and examples/app-showcase/playwright.config.ts hard-codes const PORT = 3000. It therefore exercises exactly the one port at which this bug is invisible, and it drives no connector dispatch at all. I did not file this as a finding: it is a scoping fact about a deliberately non-blocking nightly smoke rather than a defect, and it is recorded in the PR body for whoever promotes that smoke to a gate. Flagging it here so the PM can grade it rather than me sitting on the judgement.
    • Batch-mate Showcase flow comments overstate behaviour: TaskCompletedRestPingFlow / ShowcaseDeclarativeConnectorPingFlow claim the connector response is captured on the run, but neither declares an isOutput variable #7542 respected: src/automation/flows/ untouched. origin/main merged before opening the PR (the merge brought showcase dashboard changes + a new showcase test; the full suite was re-run after it, hence 19 files / 183 tests).

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions