Skip to content

Use redirectToHandler in StackHandler and disallow string default URL target#1472

Merged
N2D4 merged 3 commits into
devfrom
devin/1779471122-handler-redirect-to
May 22, 2026
Merged

Use redirectToHandler in StackHandler and disallow string default URL target#1472
N2D4 merged 3 commits into
devfrom
devin/1779471122-handler-redirect-to

Conversation

@N2D4
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 commented May 22, 2026

  • StackHandler now calls redirectToHandler(page) via internals instead of raw redirect() when a route's URL is not configured for the handler. This ensures proper cross-domain auth and redirect-back parameter handling.

  • DefaultHandlerUrlTarget no longer accepts plain strings — must use { type: "hosted" } or { type: "handler-component" }. Runtime validation throws a descriptive error for JS users who accidentally pass a string.

  • Added JSDoc documentation to HandlerUrlOptions and DefaultHandlerUrlTarget summarizing valid values for each key.

Link to Devin session: https://app.devin.ai/sessions/8db35e6835b849c38ebafde2f243fb31
Requested by: @N2D4

Summary by CodeRabbit

  • Breaking Changes

    • Default handler target must now be specified as an object with type "hosted" or "handler-component"; plain-string defaults are no longer supported.
  • New Features

    • Added a public programmatic client-side API to initiate handler redirects.
  • Improvements

    • Redirect flow now computes targets during render and performs redirects asynchronously (renders a temporary "Redirecting…" message); OAuth-callback defaulting aligned with hosted targets.

Review Change Stack

… target

- StackHandler now calls redirectToHandler(page) via the internals API
  instead of raw redirect() when a route's URL is not configured for the
  handler. This ensures proper cross-domain auth and redirect-back handling.

- DefaultHandlerUrlTarget no longer accepts plain strings. Users must use
  { type: "hosted" } or { type: "handler-component" } explicitly.
  A runtime check throws a descriptive error for JS users who pass strings.

- Added JSDoc documentation to HandlerUrlOptions and DefaultHandlerUrlTarget
  summarizing valid values for each key.

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-auth-mcp Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-auth-skills Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-backend Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-dashboard Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-demo Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-docs Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-preview-backend Ready Ready Preview, Comment May 22, 2026 8:52pm
stack-preview-dashboard Ready Ready Preview, Comment May 22, 2026 8:52pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52eaf759-ea95-4476-bcf2-369dea70627e

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5a5a6 and e5ddc0e.

📒 Files selected for processing (1)
  • packages/template/src/components-page/stack-handler-client.tsx

📝 Walkthrough

Walkthrough

Narrow DefaultHandlerUrlTarget to object-only forms, update handler URL resolution and tests, add a public redirectToHandler internals method, and refactor client-side handler redirects to compute a redirect result and perform the redirect asynchronously after render.

Changes

Default handler URL type narrowing and asynchronous redirect refactoring

Layer / File(s) Summary
Default handler URL type contract
packages/stack-shared/src/interface/handler-urls.ts
DefaultHandlerUrlTarget is narrowed to `{ type: "hosted"
URL resolution and tests
packages/template/src/lib/stack-app/url-targets.ts, packages/template/src/lib/stack-app/url-targets.test.ts
resolveHandlerUrls and resolveUnknownHandlerPathFallbackUrl now base oauth/default logic on the narrowed default target shape; tests updated to assert hosted-default resolution for signIn and oauthCallback.
Redirect handler public API
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts, packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Adds redirectToHandler(handlerName, options?) to the [stackAppInternalsSymbol] internals API (type + implementation delegating to _redirectToHandler).
Asynchronous redirect flow refactor
packages/template/src/components-page/stack-handler-client.tsx
renderComponent now accepts shouldRedirectToPage and returns { redirectToPage } for handler routes; StackHandlerClient uses shouldRedirectToPage and triggers async redirects via stackAppInternalsSymbol.redirectToHandler(..., { replace: true }) using runAsynchronouslyWithAlert, rendering a MessageCard while redirecting.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • hexclave/stack-auth#1458: Modifies resolveHandlerUrls / oauth-callback defaulting logic and overlaps in handler URL resolution behavior.

Poem

🐰 A rabbit hops through handler lanes,
Objects now guide where each path trains,
Compute the redirect, then leap away,
Async it follows after render's play,
A little hop to keep the flow in sway.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: using redirectToHandler in StackHandler and disallowing string default URL targets.
Description check ✅ Passed The description provides clear context on the changes, the reasoning (cross-domain auth handling), and JSDoc additions, though it follows a minimal template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1779471122-handler-redirect-to

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +188 to +190
if (typeof configuredUrls?.default === "string") {
throw new StackAssertionError(`The 'default' URL option must be an object (eg. { type: "hosted" } or { type: "handler-component" }), but received a string: "${configuredUrls.default}". If you meant to use hosted URLs, use { type: "hosted" } instead.`);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the runtime string check.

Comment on lines +49 to +51
* **`default`** — fallback target for every key not set individually:
* - `{ type: "handler-component" }` — use the local `StackHandler` (default).
* - `{ type: "hosted" }` — use Stack's hosted auth pages.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will probably change the default here with the next breaking version so make that clear

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated — both JSDoc blocks now note "current default, may change in the next breaking version".

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR refactors how StackHandler performs redirects when a route's URL is configured to point elsewhere: instead of calling Next.js redirect() directly in the render switch, each case now returns { redirectToPage } which is handled after rendering via redirectToHandler on the app internals, enabling proper cross-domain auth and redirect-back parameter handling. It also tightens the DefaultHandlerUrlTarget type to reject plain strings, adding a runtime StackAssertionError for JS users who pass a string.

  • stack-handler-client.tsx: redirectIfNotHandler (void side-effect) replaced by shouldRedirectToPage (boolean return), with runAsynchronously(redirectToHandler(...)) dispatched post-render — consistent with the existing pattern in client-app-impl.ts.
  • handler-urls.ts / url-targets.ts: DefaultHandlerUrlTarget no longer includes string; a runtime guard in resolveHandlerUrls throws a descriptive error for legacy JS usage; redundant typeof !== \"string\" checks removed downstream.

Confidence Score: 4/5

Safe to merge — the redirect refactor is well-contained and URL validation flows through the unchanged _redirectIfTrusted path.

The core redirect logic, type narrowing, and runtime validation are all correct. The only finding is a test name that contradicts its assertions, which could mislead future contributors but does not affect runtime behavior.

url-targets.test.ts — the renamed test description does not match what its assertions verify.

Important Files Changed

Filename Overview
packages/stack-shared/src/interface/handler-urls.ts Removes string from DefaultHandlerUrlTarget, adds JSDoc to both types — clean type-narrowing change.
packages/template/src/components-page/stack-handler-client.tsx Replaces raw redirect() calls in render with a { redirectToPage } return value that triggers redirectToHandler (with proper cross-domain and redirect-back handling) via the established runAsynchronously pattern.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Exposes redirectToHandler on the internals symbol, delegating to the existing _redirectToHandler — minimal, well-scoped addition.
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts Adds redirectToHandler to the internals interface type — matches the implementation addition exactly.
packages/template/src/lib/stack-app/url-targets.ts Adds runtime string validation for the default option; removes now-redundant typeof !== string guard after validation is moved to the entry point; resolveUnknownHandlerPathFallbackUrl parameter type tightened to DefaultHandlerUrlTarget.
packages/template/src/lib/stack-app/url-targets.test.ts Adds a test for the new string-validation throw; replaces the old string-default test with a hosted-default test, but the new test name contradicts its assertions (says does not inherit when the hosted default is in fact inherited).

Sequence Diagram

sequenceDiagram
    participant Browser
    participant StackHandlerClient
    participant renderComponent
    participant AppInternals

    Browser->>StackHandlerClient: renders /handler/sign-in
    StackHandlerClient->>renderComponent: path, shouldRedirectToPage
    renderComponent->>renderComponent: shouldRedirectToPage('signIn')?
    alt URL is not local handler target
        renderComponent-->>StackHandlerClient: "{ redirectToPage: 'signIn' }"
        StackHandlerClient->>AppInternals: "runAsynchronously(redirectToHandler('signIn', {replace:true}))"
        AppInternals->>AppInternals: planRedirectToHandler (cross-domain / redirect-back)
        AppInternals->>Browser: navigate to resolved URL
        StackHandlerClient-->>Browser: return null
    else URL is local handler target
        renderComponent-->>StackHandlerClient: SignIn component
        StackHandlerClient-->>Browser: render component
    end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/template/src/lib/stack-app/url-targets.test.ts:133-148
**Misleading test name — says "does not inherit" but asserts the opposite**

The test name `"does not inherit a hosted default target for the OAuth callback"` is the inverse of what the assertions verify. Both `urls.signIn` and `urls.oauthCallback` are expected to be hosted URLs, meaning the `{ type: "hosted" }` default IS inherited by `oauthCallback`. The old test with this pattern guarded against an absolute-string default leaking into `oauthCallback`, but now strings are disallowed and the code explicitly propagates the hosted type. A reader will trust the name over the assertions and misunderstand the intended behavior.

Reviews (1): Last reviewed commit: "Use redirectToHandler in StackHandler an..." | Re-trigger Greptile

Comment on lines 133 to 148
});

it("does not inherit a hosted default target for the OAuth callback", () => {
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");

const urls = resolveHandlerUrls({
projectId: "project-id",
urls: {
default: "https://app.example.test/handler",
default: { type: "hosted" },
},
});

expect(urls.signIn).toBe("https://app.example.test/handler");
expect(urls.oauthCallback).toBe("/handler/oauth-callback");
expect(urls.signIn).toBe("https://project-id.example-stack-hosted.test/handler/sign-in");
expect(urls.oauthCallback).toBe("https://project-id.example-stack-hosted.test/handler/oauth-callback");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading test name — says "does not inherit" but asserts the opposite

The test name "does not inherit a hosted default target for the OAuth callback" is the inverse of what the assertions verify. Both urls.signIn and urls.oauthCallback are expected to be hosted URLs, meaning the { type: "hosted" } default IS inherited by oauthCallback. The old test with this pattern guarded against an absolute-string default leaking into oauthCallback, but now strings are disallowed and the code explicitly propagates the hosted type. A reader will trust the name over the assertions and misunderstand the intended behavior.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/template/src/lib/stack-app/url-targets.test.ts
Line: 133-148

Comment:
**Misleading test name — says "does not inherit" but asserts the opposite**

The test name `"does not inherit a hosted default target for the OAuth callback"` is the inverse of what the assertions verify. Both `urls.signIn` and `urls.oauthCallback` are expected to be hosted URLs, meaning the `{ type: "hosted" }` default IS inherited by `oauthCallback`. The old test with this pattern guarded against an absolute-string default leaking into `oauthCallback`, but now strings are disallowed and the code explicitly propagates the hosted type. A reader will trust the name over the assertions and misunderstand the intended behavior.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — renamed to "inherits a hosted default target for the OAuth callback".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/template/src/lib/stack-app/url-targets.test.ts (1)

125-133: ⚡ Quick win

Use inline snapshots for the updated assertions.

These new assertions use regex/toBe; please switch to inline snapshots for consistency with the test guideline in this repo.

As per coding guidelines **/*.test.{ts,tsx,js,jsx}: Prefer .toMatchInlineSnapshot over other selectors in tests; check snapshot-serializer.ts to understand snapshot formatting and non-deterministic value handling.

Also applies to: 145-146

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/template/src/lib/stack-app/url-targets.test.ts` around lines 125 -
133, Update the test assertions to use inline snapshots: replace the
regex/toBe-based thrown-error assertions for resolveHandlerUrls (the test named
"rejects a string value for the default URL option" and the second failing
assertion around the later block) with Jest inline-snapshot matchers — e.g., use
expect(() =>
resolveHandlerUrls(...)).toThrowErrorMatchingInlineSnapshot(`"expected error
message snapshot"`) or expect(...).toMatchInlineSnapshot(`"snapshot"`) as
appropriate so the tests follow the repo's snapshot guideline and the
snapshot-serializer formatting; ensure the snapshots contain the exact error
text produced by resolveHandlerUrls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/template/src/components-page/stack-handler-client.tsx`:
- Around line 305-307: The render path in StackHandlerClient currently calls
runAsynchronously(stackApp[stackAppInternalsSymbol].redirectToHandler(...))
during render when renderComponent returns { redirectToPage }, which can trigger
repeated async redirects or silent failures; move the redirect invocation into a
React effect (useEffect) that depends on redirectToPage (matching the existing
redirect effect pattern) and call
stackApp[stackAppInternalsSymbol].redirectToHandler(redirectToPage, { replace:
true }) from inside that effect; surface failures to the user instead of only
logging by using runAsynchronouslyWithAlert (or rendering an explicit
error/fallback UI) to handle rejected promises; ensure you leave the render
return as null while the effect performs the redirect so redirects run only as a
side effect and failures are visible.

In `@packages/template/src/lib/stack-app/url-targets.test.ts`:
- Line 135: The test heading starting with it("does not inherit a hosted default
target for the OAuth callback", ...) is misleading because the assertions on
oauthCallback (lines ~145-146) expect hosted inheritance; rename the test title
to accurately state the asserted behavior (e.g., "inherits a hosted default
target for the OAuth callback" or similar) so the test name matches the
assertions in url-targets.test.ts and update only the string passed to the
it(...) call that refers to oauthCallback.

---

Nitpick comments:
In `@packages/template/src/lib/stack-app/url-targets.test.ts`:
- Around line 125-133: Update the test assertions to use inline snapshots:
replace the regex/toBe-based thrown-error assertions for resolveHandlerUrls (the
test named "rejects a string value for the default URL option" and the second
failing assertion around the later block) with Jest inline-snapshot matchers —
e.g., use expect(() =>
resolveHandlerUrls(...)).toThrowErrorMatchingInlineSnapshot(`"expected error
message snapshot"`) or expect(...).toMatchInlineSnapshot(`"snapshot"`) as
appropriate so the tests follow the repo's snapshot guideline and the
snapshot-serializer formatting; ensure the snapshots contain the exact error
text produced by resolveHandlerUrls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2aa6eb61-7edd-44f4-a9e7-c1c338ffe568

📥 Commits

Reviewing files that changed from the base of the PR and between 0c6e135 and dcffd70.

📒 Files selected for processing (6)
  • packages/stack-shared/src/interface/handler-urls.ts
  • packages/template/src/components-page/stack-handler-client.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
  • packages/template/src/lib/stack-app/url-targets.test.ts
  • packages/template/src/lib/stack-app/url-targets.ts

Comment thread packages/template/src/components-page/stack-handler-client.tsx Outdated
Comment thread packages/template/src/lib/stack-app/url-targets.test.ts Outdated
…ge, fix test name

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
…unAsynchronouslyWithAlert

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
Copilot AI review requested due to automatic review settings May 22, 2026 20:43
@N2D4 N2D4 review requested due to automatic review settings May 22, 2026 20:43
@N2D4 N2D4 merged commit 05e22e1 into dev May 22, 2026
28 of 34 checks passed
@N2D4 N2D4 deleted the devin/1779471122-handler-redirect-to branch May 22, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant