host: guarded route via boost → SX-Redirect (full nav to /login), not a #content-clobbering 303
Reported: go to an edit page, then press Home — nothing happens; navigation stops updating.
Root cause (found via a browser trace): a guarded route (host/require-login) answered a
BOOSTED (SX-Request) request with a 303 to /login. The browser's fetch follows the redirect
but DROPS the SX-Request header on the way, so /login returned the full HTML shell (<!doctype
html>…), not a text/sx fragment. Morphing that whole document into #content DESTROYS the
#content swap target (diagnostic: "#content count: 0"), so every later boosted nav fetches
but has nowhere to swap ("post-swap: root=nil") — the persistent nav Home appears to do
nothing.
Fix (host-side, no engine change — the engine already supports SX-Redirect): for a boosted
request require-login now returns 200 + an `SX-Redirect: /login?next=…` header. The engine
does a FULL navigation (browser-navigate) to a real /login page — #content is never
clobbered. Non-boosted requests still get a plain 303. Also added a "← Home" link to the
login shell (it's a standalone page with no persistent nav, so a logged-out user who followed
a guarded link was otherwise stranded — the literal "press Home" case).
TEST-FIRST: added a second boost-nav.spec.js case (home → post → click edit → assert clean
full-nav to /login, NOT a clobbered SPA, and Home works from there). Confirmed RED before
(Home did nothing on the clobbered page), GREEN after — verified on the ephemeral server AND
live. No regressions: picker 3/3 + block-editor 1/1 (login flow intact).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,10 @@
|
|||||||
(input :type "hidden" :name "next" :value (unquote next-path))
|
(input :type "hidden" :name "next" :value (unquote next-path))
|
||||||
(p (input :name "username" :placeholder "username"))
|
(p (input :name "username" :placeholder "username"))
|
||||||
(p (input :name "password" :type "password" :placeholder "password"))
|
(p (input :name "password" :type "password" :placeholder "password"))
|
||||||
(p (button :type "submit" "Log in"))))))))
|
(p (button :type "submit" "Log in")))
|
||||||
|
;; a way back into the app — the login shell is a standalone page (no persistent
|
||||||
|
;; nav), so without this a logged-out user who followed a guarded link is stranded.
|
||||||
|
(p :style "margin-top:1em" (a :href "/" "← Home")))))))
|
||||||
|
|
||||||
;; ── GET /login — login form, honouring ?next= (where to go after login) ─────
|
;; ── GET /login — login form, honouring ?next= (where to go after login) ─────
|
||||||
(define host/login-page
|
(define host/login-page
|
||||||
@@ -137,5 +140,14 @@
|
|||||||
(fn (req)
|
(fn (req)
|
||||||
(let ((principal (host/-principal-of req resolve)))
|
(let ((principal (host/-principal-of req resolve)))
|
||||||
(if (or (nil? principal) (= principal ""))
|
(if (or (nil? principal) (= principal ""))
|
||||||
(dream-redirect (str "/login?next=" (host/-safe-next (dream-path req))))
|
(let ((login-url (str "/login?next=" (host/-safe-next (dream-path req)))))
|
||||||
|
;; A BOOSTED (SX-Request) request can't be answered with a 303: the browser's
|
||||||
|
;; fetch follows the redirect WITHOUT the SX-Request header, so /login returns
|
||||||
|
;; the full HTML shell, which morphed into #content DESTROYS the SPA swap target
|
||||||
|
;; (every later boosted nav then has nowhere to swap — "nothing happens"). Return
|
||||||
|
;; an SX-Redirect header instead — the engine does a FULL navigation to /login (a
|
||||||
|
;; fresh shell). A non-boosted request still gets a plain 303.
|
||||||
|
(if (= (dream-header req "sx-request") "true")
|
||||||
|
(dream-response 200 {:sx-redirect login-url} "")
|
||||||
|
(dream-redirect login-url)))
|
||||||
(next (assoc req :dream-principal principal))))))))
|
(next (assoc req :dream-principal principal))))))))
|
||||||
|
|||||||
@@ -34,4 +34,26 @@ test.describe('boosted navigation (browser-only)', () => {
|
|||||||
expect(page.url()).not.toContain('/tags');
|
expect(page.url()).not.toContain('/tags');
|
||||||
expect(page.url()).toContain('/compose-demo/edit');
|
expect(page.url()).toContain('/compose-demo/edit');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a guarded route reached via boost does a clean full-nav to /login (no clobbered SPA), and Home works from there', async ({ page }) => {
|
||||||
|
test.setTimeout(90000);
|
||||||
|
await page.goto(BASE + '/');
|
||||||
|
await waitReady(page);
|
||||||
|
// boosted nav home -> post
|
||||||
|
await page.locator('a[href="/compose-demo/"]').first().click();
|
||||||
|
await expect(page.locator('body')).toContainText('composition object', { timeout: 15000 });
|
||||||
|
// click "edit" (guarded, logged out). A 303 would be followed by the fetch WITHOUT the
|
||||||
|
// SX-Request header -> /login returns the full shell, which morphed into #content
|
||||||
|
// DESTROYS the swap target (then nothing navigates). The fix returns SX-Redirect, so the
|
||||||
|
// engine does a FULL navigation to a real /login page.
|
||||||
|
await page.locator('a[href="/compose-demo/edit"]').click();
|
||||||
|
await page.waitForTimeout(3500);
|
||||||
|
expect(new URL(page.url()).pathname).toBe('/login');
|
||||||
|
await expect(page.locator('body')).toContainText('Log in');
|
||||||
|
// and the login page offers a way back Home that works (the reported "Home does nothing").
|
||||||
|
await page.locator('a[href="/"]').first().click();
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
await expect(page.locator('body')).toContainText('Posts', { timeout: 12000 });
|
||||||
|
expect(new URL(page.url()).pathname).toBe('/');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user