Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

plugin-form: EmbeddableForm still promises "Redirecting in 3 seconds…" after its own safety guard refused the redirect — and the redirectBlocked string it sets can never render #5073

Closed

Description

@yinlianghui

Tripped over while fixing objectui#5049 (the thank-you redirect timer's ownership). Filed unassigned and deliberately NOT fixed there: #5049's card and its claim comment both fence the countdown copy out of scope ("倒计时文案行为不动"), and PR #5070 touches only who owns the timer. Line numbers below are as of f68018d565b7c875e652f237b5413b97be5b9012.

What

packages/plugin-form/src/EmbeddableForm.tsx decides whether to redirect, but the thank-you panel's copy is keyed on something else entirely — whether a redirectUrl was authored:

  • Submit handler, line ~313: when isRedirectUrlSafe(rawRedirect, config.allowedRedirectHosts) is false, the redirect is refused with console.warn(...) plus setError(config.texts?.redirectBlocked ?? null). setSubmitted(true) has already run one statement earlier (line ~303).
  • Thank-you branch, line ~345: redirectSeconds is computed unconditionally from thankYou?.redirectDelay ?? 3000, and line ~370 renders the countdown paragraph on thankYou?.redirectUrl && ….

So an author who declares a cross-origin thankYouPage.redirectUrl without allowlisting its host gets a submitter who is told "Redirecting in 3 seconds…" and is then never redirected — the guard did its job, but the screen still promises the navigation it just refused. The panel is the terminal state of a public form, so there is nothing after it to correct the impression.

Second, smaller fact on the same path: the texts.redirectBlocked string that refusal sets is unreachable. The error banner lives in the form branch (line ~421), and the component has already switched to the thank-you branch, which renders no error at all. setError(config.texts?.redirectBlocked ?? null) is the only assignment of that key anywhere, and it only ever fires while submitted is true — so the localized string can never appear on screen. Pressing Submit Another Response (handleReset) clears it rather than showing it.

Reachability

Live, published surface: EmbeddableForm is exported from packages/plugin-form/src/index.tsx and registered as embeddable-form. Reaching it needs one authoring mistake — a redirectUrl pointing off-origin with no matching allowedRedirectHosts entry — which is exactly the case the guard was written for, and the guard is silent to the author apart from a console warning. Nothing is lost: the write has already succeeded. The harm is a false statement on the confirmation screen plus a shipped, translated string that no user can ever see.

Fix shape, if this is queued

The verdict is already available where the copy is rendered, and PR #5070 makes it explicit: after that PR the accepted destination lives in a pendingRedirect state, so the panel can key its countdown on the destination that was actually accepted instead of on the raw authored key. That is a one-condition change plus a decision this card should state rather than leave to the implementer: on a refused redirect, does the panel simply omit the countdown line, or does it surface the refusal to the submitter (and/or to the author more loudly than console.warn)? Omitting the line fixes the false promise; surfacing the refusal also makes texts.redirectBlocked mean something, but it puts an operational message in front of a member of the public, which may be the wrong audience — the author is the one who mis-declared it.

Worth pinning either way: a fixture with a non-allowlisted cross-origin redirectUrl asserting the countdown copy is absent, paired with an allowlisted one asserting it is present, so the pin cannot pass by deleting the countdown outright.

Not part of this

Whether thankYouPage.redirectUrl should be brought under the relative-path-only ruling of objectui#4989 / PR #5032 is a separate open question, recorded in #5049 and untouched here. This card is only about the panel's copy disagreeing with the component's own verdict.

Activity

  1. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    检索记录(避免重复立卡):按语义与关键词各查了一轮 open issues(EmbeddableForm / thankYouPage / redirectBlocked / 「Redirecting in N seconds」/ 致谢面板文案与 redirect 裁定不一致),命中只有 #5049 与已关闭的 #5033,无同题卡片,故新立。未指派 —— 仅记录,不占坑。

    另附一句便于分诊:本卡与 #5049 不是父子关系。#5049 的完成范围经卡面与认领评论明确划定为「定时器所有权」,倒计时文案被显式排除在外,#5049 会随 PR #5070 关闭;本卡也不依赖 #5070 才能动手(#5070 只是让「已接受的目的地」在渲染处更顺手可读),故按独立卡片立,不加 Blocked-by:。


    Generated by Claude Code

  2. self-assigned this
    on Aug 17, 2026
  3. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    认领(objectui 分片 PM 直辖,批次 22 补席)—— session session_01GTRjn8xBqp75dk7kFupVRt,分支 claude/issue-5073-countdown-verdict。os-dev 席位实施,PM 只读验收。前置 PR #5070 已落 main e354dd064(pendingRedirect state 使「已接受的目的地」在渲染处可读)。

    卡面裁定点 PM 代裁(留否决窗):倒计时行改为按实际被接受的目的地(pendingRedirect)渲染,不再按「作者是否声明了 redirectUrl」;拒绝时 —— ① 略去倒计时行(消除假承诺);② 若作者声明了 texts.redirectBlocked,在致谢面板渲染该文案 —— 这正是该键存在的场景,且是作者写给公众的话,受众由作者的声明选择,不是把运维信息塞给公众;未声明则静默略去,仅保留既有 console.warn 给作者。这同时让「已翻译字符串永不可达」的第二事实闭合。⛔ 不动 isRedirectUrlSafe / allow-list 判定本身;⛔ #4989 relative-only 并轨问题不碰(卡面已划出)。

    钉子按卡面:非白名单跨源 fixture 断言倒计时缺席(+ 声明了 redirectBlocked 时该文案在场)、白名单 fixture 断言倒计时在场 —— 成对,防「把倒计时整个删了换绿」。


    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

Labels

bugSomething isn't workingpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions