From 03bdb43abcd66f1ecfbd2d2a33440d1013445bb0 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Fri, 2 May 2025 14:10:13 +0300 Subject: [PATCH 01/37] WIP --- .../src/middleware/request-handler/index.ts | 1 + .../middleware/request-handler/request-event.ts | 14 ++++++++++++++ .../request-handler/resolve-request-handlers.ts | 3 ++- .../middleware/request-handler/rewrite-handler.ts | 5 +++++ .../src/middleware/request-handler/types.ts | 7 +++++++ 5 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts diff --git a/packages/qwik-city/src/middleware/request-handler/index.ts b/packages/qwik-city/src/middleware/request-handler/index.ts index a5d10b3195a..80a3e6dcbff 100644 --- a/packages/qwik-city/src/middleware/request-handler/index.ts +++ b/packages/qwik-city/src/middleware/request-handler/index.ts @@ -1,6 +1,7 @@ export { getErrorHtml, ServerError } from './error-handler'; export { mergeHeadersCookies } from './cookie'; export { AbortMessage, RedirectMessage } from './redirect-handler'; +export { RewriteMessage } from './rewrite-handler'; export { requestHandler } from './request-handler'; export { _TextEncoderStream_polyfill } from './polyfill'; export type { diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index d4ade181aae..32603aadcc9 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -26,6 +26,7 @@ import type { ServerRequestMode, } from './types'; import { IsQData, QDATA_JSON, QDATA_JSON_LEN } from './user-response'; +import { RewriteMessage } from './rewrite-handler'; const RequestEvLoaders = Symbol('RequestEvLoaders'); const RequestEvMode = Symbol('RequestEvMode'); @@ -220,6 +221,19 @@ export function createRequestEvent( return new RedirectMessage(); }, + rewrite: (url: string) => { + check(); + if (url) { + const fixedURL = url.replace(/([^:])\/{2,}/g, '$1/'); + if (url !== fixedURL) { + console.warn(`Rewrite URL ${url} is invalid, fixing to ${fixedURL}`); + } + headers.set('Location', fixedURL); + } + exit(); + return new RewriteMessage(); + }, + defer: (returnData) => { return typeof returnData === 'function' ? returnData : () => returnData; }, diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 37a50b9740a..03b130e6c44 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -14,6 +14,7 @@ import type { } from '../../runtime/src/types'; import { HttpStatus } from './http-status-codes'; import { RedirectMessage } from './redirect-handler'; +import { RewriteMessage } from './rewrite-handler'; import { RequestEvQwikSerializer, RequestEvSharedActionId, @@ -504,7 +505,7 @@ export async function handleRedirect(requestEv: RequestEvent) { try { await requestEv.next(); } catch (err) { - if (!(err instanceof RedirectMessage)) { + if (!(err instanceof RedirectMessage || err instanceof RewriteMessage)) { throw err; } } diff --git a/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts b/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts new file mode 100644 index 00000000000..77328eb3c1c --- /dev/null +++ b/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts @@ -0,0 +1,5 @@ +/** @public */ +export class AbortMessage {} + +/** @public */ +export class RewriteMessage extends AbortMessage {} diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index ae183b8c721..0e6bf7b39ba 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -4,6 +4,7 @@ import type { AbortMessage, RedirectMessage } from './redirect-handler'; import type { RequestEventInternal } from './request-event'; import type { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; import type { ServerError } from './error-handler'; +import type { RewriteMessage } from './rewrite-handler'; /** @public */ export interface EnvGetter { @@ -201,6 +202,12 @@ export interface RequestEventCommon */ readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; + /** + * URL to rewrite to. When called, the response will immediately end with the correct rewrite url. + * URL will remain unchanged in the browser history. + */ + readonly rewrite: (url: string) => RewriteMessage; + /** * When called, the response will immediately end with the given status code. This could be useful * to end a response with `404`, and use the 404 handler in the routes directory. See From 91cd33ece74f63f4fb8d7e5d7f36679c4ae1c3e5 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Fri, 2 May 2025 14:25:43 +0300 Subject: [PATCH 02/37] handleRewrite WIP --- .../resolve-request-handlers.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 03b130e6c44..8908efd2a6e 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -77,6 +77,7 @@ export const resolveRequestHandlers = ( } const routeModules = route[2]; requestHandlers.push(handleRedirect); + requestHandlers.push(handleRewrite); _resolveRequestHandlers( routeLoaders, routeActions, @@ -505,7 +506,7 @@ export async function handleRedirect(requestEv: RequestEvent) { try { await requestEv.next(); } catch (err) { - if (!(err instanceof RedirectMessage || err instanceof RewriteMessage)) { + if (!(err instanceof RedirectMessage)) { throw err; } } @@ -516,6 +517,7 @@ export async function handleRedirect(requestEv: RequestEvent) { const status = requestEv.status(); const location = requestEv.headers.get('Location'); const isRedirect = status >= 301 && status <= 308 && location; + if (isRedirect) { const adaptedLocation = makeQDataPath(location); if (adaptedLocation) { @@ -529,6 +531,39 @@ export async function handleRedirect(requestEv: RequestEvent) { } } +export async function handleRewrite(requestEv: RequestEvent) { + const isPageDataReq = requestEv.sharedMap.has(IsQData); + if (!isPageDataReq) { + return; + } + try { + await requestEv.next(); + } catch (err) { + if (!(err instanceof RewriteMessage)) { + throw err; + } + } + if (requestEv.headersSent) { + return; + } + + const status = requestEv.status(); + const location = requestEv.headers.get('Location'); + const isRewrite = status === 200 && location; + + if (isRewrite) { + const adaptedLocation = makeQDataPath(location); + if (adaptedLocation) { + requestEv.headers.set('Location', adaptedLocation); + requestEv.getWritableStream().close(); + return; + } else { + requestEv.status(200); + requestEv.headers.delete('Location'); + } + } +} + export async function renderQData(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); if (!isPageDataReq) { From 3e4a1cb055ce8b41b3101b6634c9ba941f280cef Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Fri, 2 May 2025 14:39:49 +0300 Subject: [PATCH 03/37] distinguish between redirect and rewrite --- packages/qwik-city/src/middleware/bun/index.ts | 2 +- .../request-handler/request-event.ts | 6 +++--- .../resolve-request-handlers.ts | 18 ++++++++++-------- packages/qwik-city/src/runtime/src/types.ts | 1 + 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/qwik-city/src/middleware/bun/index.ts b/packages/qwik-city/src/middleware/bun/index.ts index c02483581e5..25ea577d980 100644 --- a/packages/qwik-city/src/middleware/bun/index.ts +++ b/packages/qwik-city/src/middleware/bun/index.ts @@ -78,7 +78,7 @@ export function createQwikCity(opts: QwikCityBunOptions) { // bun fails to redirect if there is a body. // remove the body if there a redirect. const status = response.status; - const location = response.headers.get('Location'); + const location = response.headers.get('Redirect-Location'); const isRedirect = status >= 301 && status <= 308 && location; if (isRedirect) { return new Response(null, response); diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 32603aadcc9..2f8a9c1dbd4 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -12,6 +12,7 @@ import { createCacheControl } from './cache-control'; import { Cookie } from './cookie'; import { ServerError } from './error-handler'; import { AbortMessage, RedirectMessage } from './redirect-handler'; +import { RewriteMessage } from './rewrite-handler'; import { encoder } from './resolve-request-handlers'; import type { CacheControl, @@ -26,7 +27,6 @@ import type { ServerRequestMode, } from './types'; import { IsQData, QDATA_JSON, QDATA_JSON_LEN } from './user-response'; -import { RewriteMessage } from './rewrite-handler'; const RequestEvLoaders = Symbol('RequestEvLoaders'); const RequestEvMode = Symbol('RequestEvMode'); @@ -211,7 +211,7 @@ export function createRequestEvent( if (url !== fixedURL) { console.warn(`Redirect URL ${url} is invalid, fixing to ${fixedURL}`); } - headers.set('Location', fixedURL); + headers.set('Redirect-Location', fixedURL); } // Fallback to 'no-store' when end user is not managing Cache-Control header if (statusCode > 301 && !headers.get('Cache-Control')) { @@ -228,7 +228,7 @@ export function createRequestEvent( if (url !== fixedURL) { console.warn(`Rewrite URL ${url} is invalid, fixing to ${fixedURL}`); } - headers.set('Location', fixedURL); + headers.set('Rewrite-Location', fixedURL); } exit(); return new RewriteMessage(); diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 8908efd2a6e..abbe0098126 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -515,18 +515,18 @@ export async function handleRedirect(requestEv: RequestEvent) { } const status = requestEv.status(); - const location = requestEv.headers.get('Location'); + const location = requestEv.headers.get('Redirect-Location'); const isRedirect = status >= 301 && status <= 308 && location; if (isRedirect) { const adaptedLocation = makeQDataPath(location); if (adaptedLocation) { - requestEv.headers.set('Location', adaptedLocation); + requestEv.headers.set('Redirect-Location', adaptedLocation); requestEv.getWritableStream().close(); return; } else { requestEv.status(200); - requestEv.headers.delete('Location'); + requestEv.headers.delete('Redirect-Location'); } } } @@ -548,18 +548,18 @@ export async function handleRewrite(requestEv: RequestEvent) { } const status = requestEv.status(); - const location = requestEv.headers.get('Location'); + const location = requestEv.headers.get('Rewrite-Location'); const isRewrite = status === 200 && location; if (isRewrite) { const adaptedLocation = makeQDataPath(location); if (adaptedLocation) { - requestEv.headers.set('Location', adaptedLocation); + requestEv.headers.set('Rewrite-Location', adaptedLocation); requestEv.getWritableStream().close(); return; } else { requestEv.status(200); - requestEv.headers.delete('Location'); + requestEv.headers.delete('Rewrite-Location'); } } } @@ -576,7 +576,8 @@ export async function renderQData(requestEv: RequestEvent) { } const status = requestEv.status(); - const location = requestEv.headers.get('Location'); + const redirectLocation = requestEv.headers.get('Redirect-Location'); + const rewriteLocation = requestEv.headers.get('Rewrite-Location'); const trailingSlash = getRequestTrailingSlash(requestEv); const requestHeaders: Record = {}; @@ -588,7 +589,8 @@ export async function renderQData(requestEv: RequestEvent) { action: requestEv.sharedMap.get(RequestEvSharedActionId), status: status !== 200 ? status : 200, href: getPathname(requestEv.url, trailingSlash), - redirect: location ?? undefined, + redirect: redirectLocation ?? undefined, + rewrite: rewriteLocation ?? undefined, }; const writer = requestEv.getWritableStream().getWriter(); const qwikSerializer = (requestEv as RequestEventInternal)[RequestEvQwikSerializer]; diff --git a/packages/qwik-city/src/runtime/src/types.ts b/packages/qwik-city/src/runtime/src/types.ts index 12845d7c9af..a0127e289ce 100644 --- a/packages/qwik-city/src/runtime/src/types.ts +++ b/packages/qwik-city/src/runtime/src/types.ts @@ -307,6 +307,7 @@ export interface ClientPageData extends Omit { status: number; href: string; redirect?: string; + rewrite?: string; } /** @public */ From f75d60a711d78c8c4568d2289f443c2ae41239f7 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Fri, 2 May 2025 19:30:29 +0300 Subject: [PATCH 04/37] header property Location is a specific header meant for redirects, removing it breaks redirects --- packages/qwik-city/src/middleware/bun/index.ts | 2 +- .../src/middleware/request-handler/request-event.ts | 2 +- .../request-handler/resolve-request-handlers.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/qwik-city/src/middleware/bun/index.ts b/packages/qwik-city/src/middleware/bun/index.ts index 25ea577d980..c02483581e5 100644 --- a/packages/qwik-city/src/middleware/bun/index.ts +++ b/packages/qwik-city/src/middleware/bun/index.ts @@ -78,7 +78,7 @@ export function createQwikCity(opts: QwikCityBunOptions) { // bun fails to redirect if there is a body. // remove the body if there a redirect. const status = response.status; - const location = response.headers.get('Redirect-Location'); + const location = response.headers.get('Location'); const isRedirect = status >= 301 && status <= 308 && location; if (isRedirect) { return new Response(null, response); diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 2f8a9c1dbd4..9e21f09978c 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -211,7 +211,7 @@ export function createRequestEvent( if (url !== fixedURL) { console.warn(`Redirect URL ${url} is invalid, fixing to ${fixedURL}`); } - headers.set('Redirect-Location', fixedURL); + headers.set('Location', fixedURL); } // Fallback to 'no-store' when end user is not managing Cache-Control header if (statusCode > 301 && !headers.get('Cache-Control')) { diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index abbe0098126..195bebf9e91 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -515,18 +515,18 @@ export async function handleRedirect(requestEv: RequestEvent) { } const status = requestEv.status(); - const location = requestEv.headers.get('Redirect-Location'); + const location = requestEv.headers.get('Location'); const isRedirect = status >= 301 && status <= 308 && location; if (isRedirect) { const adaptedLocation = makeQDataPath(location); if (adaptedLocation) { - requestEv.headers.set('Redirect-Location', adaptedLocation); + requestEv.headers.set('Location', adaptedLocation); requestEv.getWritableStream().close(); return; } else { requestEv.status(200); - requestEv.headers.delete('Redirect-Location'); + requestEv.headers.delete('Location'); } } } @@ -576,7 +576,7 @@ export async function renderQData(requestEv: RequestEvent) { } const status = requestEv.status(); - const redirectLocation = requestEv.headers.get('Redirect-Location'); + const redirectLocation = requestEv.headers.get('Location'); const rewriteLocation = requestEv.headers.get('Rewrite-Location'); const trailingSlash = getRequestTrailingSlash(requestEv); From d5fe0d4c6e766d760c45c005cce4ede62f55fb35 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Fri, 2 May 2025 20:51:43 +0300 Subject: [PATCH 05/37] rewrite finally doesnt throw error > behaves as redirect if the status isnt a 301+. still WIP, need to remove consoles. --- packages/qwik-city/src/buildtime/vite/dev-server.ts | 8 ++++++++ .../request-handler/resolve-request-handlers.ts | 13 +++++++++++++ .../src/middleware/request-handler/user-response.ts | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/packages/qwik-city/src/buildtime/vite/dev-server.ts b/packages/qwik-city/src/buildtime/vite/dev-server.ts index 310d60ede56..80a19f526b9 100644 --- a/packages/qwik-city/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/src/buildtime/vite/dev-server.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { QwikViteDevResponse } from '@builder.io/qwik/optimizer'; import fs from 'node:fs'; import type { ServerResponse } from 'node:http'; @@ -143,6 +144,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { return async (req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => { try { + console.log('OMER___ssrDevMiddleware - try'); const url = getUrl(req, computeOrigin(req)); if (shouldSkipRequest(url.pathname) || isVitePing(url.pathname, req.headers)) { @@ -169,6 +171,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { const routeModulePaths = new WeakMap(); try { + console.log('OMER___ssrDevMiddleware - resolveRoute', url); const { serverPlugins, loadedRoute } = await resolveRoute(routeModulePaths, url); const renderFn = async (requestEv: RequestEvent) => { @@ -211,6 +214,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { } }; + console.log('OMER___ssrDevMiddleware - resolveRequestHandlers', url); const requestHandlers = resolveRequestHandlers( serverPlugins, loadedRoute, @@ -219,7 +223,9 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { renderFn ); + console.log('OMER___ssrDevMiddleware - requestHandlers', requestHandlers); if (requestHandlers.length > 0) { + console.log('OMER___ssrDevMiddleware - requestHandlers.length > 0'); const serverRequestEv = await fromNodeHttp(url, req, res, 'dev'); Object.assign(serverRequestEv.platform, ctx.opts.platform); @@ -244,6 +250,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { return; } } else { + console.log('OMER___ssrDevMiddleware - requestHandlers.length === 0'); // no matching route // test if this is a dev service-worker.js request @@ -257,6 +264,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { } } } catch (e: any) { + console.log('OMER___ssrDevMiddleware - catch', `${e}`); if (e instanceof Error) { server.ssrFixStacktrace(e); formatError(e); diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 195bebf9e91..1e80b301d0d 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { QRL } from '@builder.io/qwik'; import type { Render, RenderToStringResult } from '@builder.io/qwik/server'; import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; @@ -46,7 +47,10 @@ export const resolveRequestHandlers = ( const routeActions: ActionInternal[] = []; const requestHandlers: RequestHandler[] = []; + const isPageRoute = !!(route && isLastModulePageRoute(route[2])); + console.log('resolveRequestHandlers - isPageRoute', isPageRoute); + if (serverPlugins) { _resolveRequestHandlers( routeLoaders, @@ -77,6 +81,8 @@ export const resolveRequestHandlers = ( } const routeModules = route[2]; requestHandlers.push(handleRedirect); + + console.log('resolveRequestHandlers - handleRewrite', route, routeName); requestHandlers.push(handleRewrite); _resolveRequestHandlers( routeLoaders, @@ -501,8 +507,11 @@ export function renderQwikMiddleware(render: Render) { export async function handleRedirect(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); if (!isPageDataReq) { + console.log('handleRedirect - isPageDataReq=false'); return; } + console.log('handleRedirect - isPageDataReq=true'); + try { await requestEv.next(); } catch (err) { @@ -534,8 +543,11 @@ export async function handleRedirect(requestEv: RequestEvent) { export async function handleRewrite(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); if (!isPageDataReq) { + console.log('handleRewrite - isPageDataReq=false'); + return; } + console.log('handleRewrite - isPageDataReq=true'); try { await requestEv.next(); } catch (err) { @@ -566,6 +578,7 @@ export async function handleRewrite(requestEv: RequestEvent) { export async function renderQData(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); + console.log('renderQData - isPageDataReq', isPageDataReq); if (!isPageDataReq) { return; } diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 6f240dbc157..6ec1087c17a 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city'; import type { LoadedRoute } from '../../runtime/src/types'; import { ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; @@ -10,6 +11,7 @@ import { } from './request-event'; import { encoder } from './resolve-request-handlers'; import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; +import { RewriteMessage } from './rewrite-handler'; export interface QwikCityRun { response: Promise; @@ -39,6 +41,7 @@ export function runQwikCity( basePathname = '/', qwikSerializer: QwikSerializer ): QwikCityRun { + console.log('runQwikCity - start', loadedRoute); let resolve: (value: T) => void; const responsePromise = new Promise((r) => (resolve = r)); const requestEv = createRequestEvent( @@ -65,6 +68,11 @@ async function runNext(requestEv: RequestEventInternal, resolve: (value: any) => await requestEv.next(); } catch (e) { if (e instanceof RedirectMessage) { + console.log('OMER___runNext - RedirectMessage'); + const stream = requestEv.getWritableStream(); + await stream.close(); + } else if (e instanceof RewriteMessage) { + console.log('OMER___runNext - RewriteMessage'); const stream = requestEv.getWritableStream(); await stream.close(); } else if (e instanceof ServerError) { From 754aba3176a9b625b1a40203e9a3606253e9c9ae Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 17:31:05 +0300 Subject: [PATCH 06/37] WIP - rewrite somewhat works --- .../src/buildtime/vite/dev-server.ts | 17 +++ .../request-handler/request-event.ts | 36 +++-- .../src/middleware/request-handler/types.ts | 6 +- .../request-handler/user-response.ts | 125 +++++++++++------- .../src/runtime/src/qwik-city-component.tsx | 2 + 5 files changed, 128 insertions(+), 58 deletions(-) diff --git a/packages/qwik-city/src/buildtime/vite/dev-server.ts b/packages/qwik-city/src/buildtime/vite/dev-server.ts index 80a19f526b9..ba12f4a8c53 100644 --- a/packages/qwik-city/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/src/buildtime/vite/dev-server.ts @@ -233,10 +233,27 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { await server.ssrLoadModule('@qwik-serializer'); const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable }; + const applyRewrite = async (url: URL) => { + const { serverPlugins, loadedRoute } = await resolveRoute(routeModulePaths, url); + const requestHandlers = resolveRequestHandlers( + serverPlugins, + loadedRoute, + req.method ?? 'GET', + false, + renderFn + ); + + return { + loadedRoute, + requestHandlers, + }; + }; + const { completion, requestEv } = runQwikCity( serverRequestEv, loadedRoute, requestHandlers, + applyRewrite, ctx.opts.trailingSlash, ctx.opts.basePathname, qwikSerializer diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 9e21f09978c..bc3f613774b 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { ValueOrPromise } from '@builder.io/qwik'; import { QDATA_KEY } from '../../runtime/src/constants'; import type { @@ -67,11 +68,21 @@ export function createRequestEvent( let locale = serverRequestEv.locale; let status = 200; - const next = async () => { + const next = async (_loadedRoute = loadedRoute, _requestHandlers = requestHandlers) => { + console.log('OMER___next - start', _requestHandlers); routeModuleIndex++; - while (routeModuleIndex < requestHandlers.length) { - const moduleRequestHandler = requestHandlers[routeModuleIndex]; + // Replace loadedRoute with _loadedRoute incase of a rewrite. + if (loadedRoute !== _loadedRoute && _loadedRoute !== null && loadedRoute !== null) { + for (let i = 0; i < _loadedRoute.length; i++) { + loadedRoute[i] = _loadedRoute[i]; + } + loadedRoute.splice(_loadedRoute.length); + } + + while (routeModuleIndex < _requestHandlers.length) { + console.log('OMER___next - routeModuleIndex', routeModuleIndex); + const moduleRequestHandler = _requestHandlers[routeModuleIndex]; const asyncStore = globalThis.qcAsyncRequestStore; const result = asyncStore?.run ? asyncStore.run(requestEv, moduleRequestHandler, requestEv) @@ -81,6 +92,8 @@ export function createRequestEvent( } routeModuleIndex++; } + + console.log('OMER___next - end'); }; const check = () => { @@ -221,16 +234,17 @@ export function createRequestEvent( return new RedirectMessage(); }, - rewrite: (url: string) => { + rewrite: (_url: string) => { + console.log('OMER___rewrite - start'); check(); - if (url) { - const fixedURL = url.replace(/([^:])\/{2,}/g, '$1/'); - if (url !== fixedURL) { - console.warn(`Rewrite URL ${url} is invalid, fixing to ${fixedURL}`); - } - headers.set('Rewrite-Location', fixedURL); + const fixedURL = _url.replace(/([^:])\/{2,}/g, '$1/'); + if (_url !== fixedURL) { + console.warn(`Rewrite URL ${_url} is invalid, fixing to ${fixedURL}`); } - exit(); + url.pathname = fixedURL; + headers.set('Rewrite-Location', fixedURL); + // should be restarted! + routeModuleIndex = -1; return new RewriteMessage(); }, diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 0e6bf7b39ba..01518f577cf 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -5,6 +5,7 @@ import type { RequestEventInternal } from './request-event'; import type { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; import type { ServerError } from './error-handler'; import type { RewriteMessage } from './rewrite-handler'; +import type { LoadedRoute } from '../../runtime/src/types'; /** @public */ export interface EnvGetter { @@ -456,7 +457,10 @@ export interface RequestEvent extends RequestEventC * * NOTE: Ensure that the call to `next()` is `await`ed. */ - readonly next: () => Promise; + readonly next: ( + loadedRoute?: LoadedRoute | null, + requestHandlers?: RequestHandler[] + ) => Promise; } declare global { diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 6ec1087c17a..e7f19924583 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -37,6 +37,9 @@ export function runQwikCity( serverRequestEv: ServerRequestEvent, loadedRoute: LoadedRoute | null, requestHandlers: RequestHandler[], + applyRewrite: ( + url: URL + ) => Promise<{ loadedRoute: LoadedRoute; requestHandlers: RequestHandler[] }>, trailingSlash = true, basePathname = '/', qwikSerializer: QwikSerializer @@ -53,67 +56,97 @@ export function runQwikCity( qwikSerializer, resolve! ); + return { response: responsePromise, requestEv, completion: asyncStore - ? asyncStore.run(requestEv, runNext, requestEv, resolve!) - : runNext(requestEv, resolve!), + ? asyncStore.run(requestEv, runNext, requestEv, applyRewrite, resolve!) + : runNext(requestEv, applyRewrite, resolve!), }; } -async function runNext(requestEv: RequestEventInternal, resolve: (value: any) => void) { - try { - // Run all middlewares - await requestEv.next(); - } catch (e) { - if (e instanceof RedirectMessage) { - console.log('OMER___runNext - RedirectMessage'); - const stream = requestEv.getWritableStream(); - await stream.close(); - } else if (e instanceof RewriteMessage) { - console.log('OMER___runNext - RewriteMessage'); - const stream = requestEv.getWritableStream(); - await stream.close(); - } else if (e instanceof ServerError) { - if (!requestEv.headersSent) { - const status = e.status as StatusCodes; - const accept = requestEv.request.headers.get('Accept'); - if (accept && !accept.includes('text/html')) { - const qwikSerializer = requestEv[RequestEvQwikSerializer]; - requestEv.headers.set('Content-Type', 'application/qwik-json'); - requestEv.send(status, await qwikSerializer._serializeData(e.data, true)); - } else { - const html = getErrorHtml(e.status, e.data); - requestEv.html(status, html); +async function runNext( + requestEv: RequestEventInternal, + applyRewrite: ( + url: URL + ) => Promise<{ loadedRoute: LoadedRoute; requestHandlers: RequestHandler[] }>, + resolve: (value: any) => void +) { + async function _runNext( + resolve: (value: any) => void, + loadedRoute?: LoadedRoute | null, + requestHandlers?: RequestHandler[], + rewriteAttempt = 1 + ) { + try { + try { + // Run all middlewares + await requestEv.next(loadedRoute, requestHandlers); + } catch (e) { + if (e instanceof RewriteMessage) { + console.log('OMER___runNext - RewriteMessage'); + + if (rewriteAttempt > 5) { + throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); + } + + const url = new URL(requestEv.url); + url.pathname = requestEv.headers.get('Rewrite-Location')!; + console.log('OMER___runNext - RewriteMessage - url', url); + const { loadedRoute, requestHandlers } = await applyRewrite(url); + return await _runNext(resolve, loadedRoute, requestHandlers, rewriteAttempt + 1); } + + throw e; } - } else if (!(e instanceof AbortMessage)) { - if (getRequestMode(requestEv) !== 'dev') { - try { - if (!requestEv.headersSent) { - requestEv.headers.set('content-type', 'text/html; charset=utf-8'); - requestEv.cacheControl({ noCache: true }); - requestEv.status(500); + } catch (e) { + if (e instanceof RedirectMessage) { + console.log('OMER___runNext - RedirectMessage'); + const stream = requestEv.getWritableStream(); + await stream.close(); + } else if (e instanceof ServerError) { + if (!requestEv.headersSent) { + const status = e.status as StatusCodes; + const accept = requestEv.request.headers.get('Accept'); + if (accept && !accept.includes('text/html')) { + const qwikSerializer = requestEv[RequestEvQwikSerializer]; + requestEv.headers.set('Content-Type', 'application/qwik-json'); + requestEv.send(status, await qwikSerializer._serializeData(e.data, true)); + } else { + const html = getErrorHtml(e.status, e.data); + requestEv.html(status, html); } - const stream = requestEv.getWritableStream(); - if (!stream.locked) { - const writer = stream.getWriter(); - await writer.write(encoder.encode(minimalHtmlResponse(500, 'Internal Server Error'))); - await writer.close(); + } + } else if (!(e instanceof AbortMessage)) { + if (getRequestMode(requestEv) !== 'dev') { + try { + if (!requestEv.headersSent) { + requestEv.headers.set('content-type', 'text/html; charset=utf-8'); + requestEv.cacheControl({ noCache: true }); + requestEv.status(500); + } + const stream = requestEv.getWritableStream(); + if (!stream.locked) { + const writer = stream.getWriter(); + await writer.write(encoder.encode(minimalHtmlResponse(500, 'Internal Server Error'))); + await writer.close(); + } + } catch { + console.error('Unable to render error page'); } - } catch { - console.error('Unable to render error page'); } + return e; + } + } finally { + if (!requestEv.isDirty()) { + resolve(null); } - return e; - } - } finally { - if (!requestEv.isDirty()) { - resolve(null); } + return undefined; } - return undefined; + + return _runNext(resolve); } /** diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index 49f801648d8..e3eb2a3cda7 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { $, component$, @@ -365,6 +366,7 @@ export const QwikCityProvider = component$((props) => { if (loadedRoute) { const [routeName, params, mods, menu] = loadedRoute; + console.log('OMER___loadedRoute', routeName, params, mods, menu); const contentModules = mods as ContentModule[]; const pageModule = contentModules[contentModules.length - 1] as PageModule; From 2122de285295f7362a32fc1d2b3bdfe406a7025d Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 21:14:49 +0300 Subject: [PATCH 07/37] Rewrite works. --- packages/qwik-city/src/runtime/src/qwik-city-component.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index e3eb2a3cda7..397f22a4a1b 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -347,12 +347,14 @@ export const QwikCityProvider = component$((props) => { const newURL = new URL(newHref, trackUrl); if (!isSamePath(newURL, trackUrl)) { // Change our path to the canonical path in the response. - trackUrl = newURL; + // TODO - fix it asap with better logic! it works but it's probably the worst way of doing it. + trackUrl = pageData.rewrite ? new URL(trackUrl) : newURL; + loadRoutePromise = loadRoute( qwikCity.routes, qwikCity.menus, qwikCity.cacheModules, - trackUrl.pathname + newURL.pathname ); } From 15ec39384564fe659a2683fe600b361e7fb963ad Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 23:19:14 +0300 Subject: [PATCH 08/37] remove console logs --- packages/qwik-city/src/buildtime/vite/dev-server.ts | 8 -------- .../src/middleware/request-handler/request-event.ts | 8 -------- .../request-handler/resolve-request-handlers.ts | 9 --------- .../src/middleware/request-handler/user-response.ts | 4 ---- .../qwik-city/src/runtime/src/qwik-city-component.tsx | 2 -- 5 files changed, 31 deletions(-) diff --git a/packages/qwik-city/src/buildtime/vite/dev-server.ts b/packages/qwik-city/src/buildtime/vite/dev-server.ts index ba12f4a8c53..9daad16a351 100644 --- a/packages/qwik-city/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/src/buildtime/vite/dev-server.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { QwikViteDevResponse } from '@builder.io/qwik/optimizer'; import fs from 'node:fs'; import type { ServerResponse } from 'node:http'; @@ -144,7 +143,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { return async (req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => { try { - console.log('OMER___ssrDevMiddleware - try'); const url = getUrl(req, computeOrigin(req)); if (shouldSkipRequest(url.pathname) || isVitePing(url.pathname, req.headers)) { @@ -171,7 +169,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { const routeModulePaths = new WeakMap(); try { - console.log('OMER___ssrDevMiddleware - resolveRoute', url); const { serverPlugins, loadedRoute } = await resolveRoute(routeModulePaths, url); const renderFn = async (requestEv: RequestEvent) => { @@ -214,7 +211,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { } }; - console.log('OMER___ssrDevMiddleware - resolveRequestHandlers', url); const requestHandlers = resolveRequestHandlers( serverPlugins, loadedRoute, @@ -223,9 +219,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { renderFn ); - console.log('OMER___ssrDevMiddleware - requestHandlers', requestHandlers); if (requestHandlers.length > 0) { - console.log('OMER___ssrDevMiddleware - requestHandlers.length > 0'); const serverRequestEv = await fromNodeHttp(url, req, res, 'dev'); Object.assign(serverRequestEv.platform, ctx.opts.platform); @@ -267,7 +261,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { return; } } else { - console.log('OMER___ssrDevMiddleware - requestHandlers.length === 0'); // no matching route // test if this is a dev service-worker.js request @@ -281,7 +274,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { } } } catch (e: any) { - console.log('OMER___ssrDevMiddleware - catch', `${e}`); if (e instanceof Error) { server.ssrFixStacktrace(e); formatError(e); diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index bc3f613774b..d5b9410b43c 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { ValueOrPromise } from '@builder.io/qwik'; import { QDATA_KEY } from '../../runtime/src/constants'; import type { @@ -69,7 +68,6 @@ export function createRequestEvent( let status = 200; const next = async (_loadedRoute = loadedRoute, _requestHandlers = requestHandlers) => { - console.log('OMER___next - start', _requestHandlers); routeModuleIndex++; // Replace loadedRoute with _loadedRoute incase of a rewrite. @@ -81,7 +79,6 @@ export function createRequestEvent( } while (routeModuleIndex < _requestHandlers.length) { - console.log('OMER___next - routeModuleIndex', routeModuleIndex); const moduleRequestHandler = _requestHandlers[routeModuleIndex]; const asyncStore = globalThis.qcAsyncRequestStore; const result = asyncStore?.run @@ -92,8 +89,6 @@ export function createRequestEvent( } routeModuleIndex++; } - - console.log('OMER___next - end'); }; const check = () => { @@ -222,7 +217,6 @@ export function createRequestEvent( if (url) { const fixedURL = url.replace(/([^:])\/{2,}/g, '$1/'); if (url !== fixedURL) { - console.warn(`Redirect URL ${url} is invalid, fixing to ${fixedURL}`); } headers.set('Location', fixedURL); } @@ -235,11 +229,9 @@ export function createRequestEvent( }, rewrite: (_url: string) => { - console.log('OMER___rewrite - start'); check(); const fixedURL = _url.replace(/([^:])\/{2,}/g, '$1/'); if (_url !== fixedURL) { - console.warn(`Rewrite URL ${_url} is invalid, fixing to ${fixedURL}`); } url.pathname = fixedURL; headers.set('Rewrite-Location', fixedURL); diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 1e80b301d0d..e8d4f248f04 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { QRL } from '@builder.io/qwik'; import type { Render, RenderToStringResult } from '@builder.io/qwik/server'; import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; @@ -49,7 +48,6 @@ export const resolveRequestHandlers = ( const requestHandlers: RequestHandler[] = []; const isPageRoute = !!(route && isLastModulePageRoute(route[2])); - console.log('resolveRequestHandlers - isPageRoute', isPageRoute); if (serverPlugins) { _resolveRequestHandlers( @@ -82,7 +80,6 @@ export const resolveRequestHandlers = ( const routeModules = route[2]; requestHandlers.push(handleRedirect); - console.log('resolveRequestHandlers - handleRewrite', route, routeName); requestHandlers.push(handleRewrite); _resolveRequestHandlers( routeLoaders, @@ -507,10 +504,8 @@ export function renderQwikMiddleware(render: Render) { export async function handleRedirect(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); if (!isPageDataReq) { - console.log('handleRedirect - isPageDataReq=false'); return; } - console.log('handleRedirect - isPageDataReq=true'); try { await requestEv.next(); @@ -543,11 +538,8 @@ export async function handleRedirect(requestEv: RequestEvent) { export async function handleRewrite(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); if (!isPageDataReq) { - console.log('handleRewrite - isPageDataReq=false'); - return; } - console.log('handleRewrite - isPageDataReq=true'); try { await requestEv.next(); } catch (err) { @@ -578,7 +570,6 @@ export async function handleRewrite(requestEv: RequestEvent) { export async function renderQData(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); - console.log('renderQData - isPageDataReq', isPageDataReq); if (!isPageDataReq) { return; } diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index e7f19924583..ace2a850fa1 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -85,15 +85,12 @@ async function runNext( await requestEv.next(loadedRoute, requestHandlers); } catch (e) { if (e instanceof RewriteMessage) { - console.log('OMER___runNext - RewriteMessage'); - if (rewriteAttempt > 5) { throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); } const url = new URL(requestEv.url); url.pathname = requestEv.headers.get('Rewrite-Location')!; - console.log('OMER___runNext - RewriteMessage - url', url); const { loadedRoute, requestHandlers } = await applyRewrite(url); return await _runNext(resolve, loadedRoute, requestHandlers, rewriteAttempt + 1); } @@ -102,7 +99,6 @@ async function runNext( } } catch (e) { if (e instanceof RedirectMessage) { - console.log('OMER___runNext - RedirectMessage'); const stream = requestEv.getWritableStream(); await stream.close(); } else if (e instanceof ServerError) { diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index 397f22a4a1b..1b2d152b9cd 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { $, component$, @@ -368,7 +367,6 @@ export const QwikCityProvider = component$((props) => { if (loadedRoute) { const [routeName, params, mods, menu] = loadedRoute; - console.log('OMER___loadedRoute', routeName, params, mods, menu); const contentModules = mods as ContentModule[]; const pageModule = contentModules[contentModules.length - 1] as PageModule; From 58833619fe7338fc183d21927324def99a1bff17 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 23:30:22 +0300 Subject: [PATCH 09/37] fix unreadable condition + remove leftover log --- .../src/middleware/request-handler/user-response.ts | 2 -- .../qwik-city/src/runtime/src/qwik-city-component.tsx | 9 +++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index ace2a850fa1..95b457946e8 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city'; import type { LoadedRoute } from '../../runtime/src/types'; import { ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; @@ -44,7 +43,6 @@ export function runQwikCity( basePathname = '/', qwikSerializer: QwikSerializer ): QwikCityRun { - console.log('runQwikCity - start', loadedRoute); let resolve: (value: T) => void; const responsePromise = new Promise((r) => (resolve = r)); const requestEv = createRequestEvent( diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index 1b2d152b9cd..14a695fe132 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -345,15 +345,16 @@ export const QwikCityProvider = component$((props) => { const newHref = pageData.href; const newURL = new URL(newHref, trackUrl); if (!isSamePath(newURL, trackUrl)) { - // Change our path to the canonical path in the response. - // TODO - fix it asap with better logic! it works but it's probably the worst way of doing it. - trackUrl = pageData.rewrite ? new URL(trackUrl) : newURL; + // Change our path to the canonical path in the response unless rewrite. + if (!pageData.rewrite) { + trackUrl = newURL; + } loadRoutePromise = loadRoute( qwikCity.routes, qwikCity.menus, qwikCity.cacheModules, - newURL.pathname + newURL.pathname // Load the canonical path. ); } From 5eab908cbe9e6f95ca8a713e086f8271e08698cf Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 23:33:46 +0300 Subject: [PATCH 10/37] bring back accidental deleted log --- .../qwik-city/src/middleware/request-handler/request-event.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index d5b9410b43c..c46400b81a9 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -217,6 +217,7 @@ export function createRequestEvent( if (url) { const fixedURL = url.replace(/([^:])\/{2,}/g, '$1/'); if (url !== fixedURL) { + console.warn(`Redirect URL ${url} is invalid, fixing to ${fixedURL}`); } headers.set('Location', fixedURL); } @@ -232,6 +233,7 @@ export function createRequestEvent( check(); const fixedURL = _url.replace(/([^:])\/{2,}/g, '$1/'); if (_url !== fixedURL) { + console.warn(`Rewrite URL ${_url} is invalid, fixing to ${fixedURL}`); } url.pathname = fixedURL; headers.set('Rewrite-Location', fixedURL); From 0d4e8f31cc99f4ae0a4fb1a4f3ff5651401dc4df Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 23:44:41 +0300 Subject: [PATCH 11/37] tidy up before tests --- .../src/buildtime/vite/dev-server.ts | 3 ++- .../request-handler/request-handler.ts | 22 ++++++++++++++++++- .../request-handler/user-response.ts | 10 +++------ packages/qwik-city/src/runtime/src/types.ts | 4 ++++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/qwik-city/src/buildtime/vite/dev-server.ts b/packages/qwik-city/src/buildtime/vite/dev-server.ts index 9daad16a351..c24ca130f7e 100644 --- a/packages/qwik-city/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/src/buildtime/vite/dev-server.ts @@ -18,6 +18,7 @@ import { matchRoute } from '../../runtime/src/route-matcher'; import { getMenuLoader } from '../../runtime/src/routing'; import type { ActionInternal, + ApplyRewriteInternal, ContentMenu, LoadedRoute, LoaderInternal, @@ -227,7 +228,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { await server.ssrLoadModule('@qwik-serializer'); const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable }; - const applyRewrite = async (url: URL) => { + const applyRewrite: ApplyRewriteInternal = async (url: URL) => { const { serverPlugins, loadedRoute } = await resolveRoute(routeModulePaths, url); const requestHandlers = resolveRequestHandlers( serverPlugins, diff --git a/packages/qwik-city/src/middleware/request-handler/request-handler.ts b/packages/qwik-city/src/middleware/request-handler/request-handler.ts index 02ab430732d..4bb17fc0e13 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-handler.ts @@ -1,6 +1,6 @@ import type { Render } from '@builder.io/qwik/server'; import { loadRoute } from '../../runtime/src/routing'; -import type { QwikCityPlan } from '../../runtime/src/types'; +import type { ApplyRewriteInternal, QwikCityPlan } from '../../runtime/src/types'; import { renderQwikMiddleware, resolveRequestHandlers } from './resolve-request-handlers'; import type { QwikSerializer, ServerRenderOptions, ServerRequestEvent } from './types'; import { getRouteMatchPathname, runQwikCity, type QwikCityRun } from './user-response'; @@ -25,12 +25,32 @@ export async function requestHandler( checkOrigin ?? true, render ); + if (routeAndHandlers) { const [route, requestHandlers] = routeAndHandlers; + + const applyRewrite: ApplyRewriteInternal = async (url: URL) => { + const routeAndHandlers = await loadRequestHandlers( + qwikCityPlan, + url.pathname, + serverRequestEv.request.method, + checkOrigin ?? true, + render + ); + + if (routeAndHandlers) { + const [loadedRoute, requestHandlers] = routeAndHandlers; + return { loadedRoute, requestHandlers }; + } else { + return { loadedRoute: null, requestHandlers: [] }; + } + }; + return runQwikCity( serverRequestEv, route, requestHandlers, + applyRewrite, qwikCityPlan.trailingSlash, qwikCityPlan.basePathname, qwikSerializer diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 95b457946e8..544003898cf 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -1,5 +1,5 @@ import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city'; -import type { LoadedRoute } from '../../runtime/src/types'; +import type { ApplyRewriteInternal, LoadedRoute } from '../../runtime/src/types'; import { ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import { @@ -36,9 +36,7 @@ export function runQwikCity( serverRequestEv: ServerRequestEvent, loadedRoute: LoadedRoute | null, requestHandlers: RequestHandler[], - applyRewrite: ( - url: URL - ) => Promise<{ loadedRoute: LoadedRoute; requestHandlers: RequestHandler[] }>, + applyRewrite: ApplyRewriteInternal, trailingSlash = true, basePathname = '/', qwikSerializer: QwikSerializer @@ -66,9 +64,7 @@ export function runQwikCity( async function runNext( requestEv: RequestEventInternal, - applyRewrite: ( - url: URL - ) => Promise<{ loadedRoute: LoadedRoute; requestHandlers: RequestHandler[] }>, + applyRewrite: ApplyRewriteInternal, resolve: (value: any) => void ) { async function _runNext( diff --git a/packages/qwik-city/src/runtime/src/types.ts b/packages/qwik-city/src/runtime/src/types.ts index a0127e289ce..b75e77b1c73 100644 --- a/packages/qwik-city/src/runtime/src/types.ts +++ b/packages/qwik-city/src/runtime/src/types.ts @@ -81,6 +81,10 @@ export type RouteStateInternal = { scroll?: boolean; }; +export type ApplyRewriteInternal = ( + url: URL +) => Promise<{ loadedRoute: LoadedRoute | null; requestHandlers: RequestHandler[] }>; + /** * @param url - The URL that the user is trying to navigate to, or a number to indicate the user is * trying to navigate back/forward in the application history. If it is missing, the event is sent From d335cc65b00eac3d4d4b5faa3ed76c5b00e766cd Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 23:54:27 +0300 Subject: [PATCH 12/37] support rewrite for URL objects --- .../qwik-city/src/middleware/request-handler/request-event.ts | 4 +++- packages/qwik-city/src/middleware/request-handler/types.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index c46400b81a9..addca80b063 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -229,8 +229,10 @@ export function createRequestEvent( return new RedirectMessage(); }, - rewrite: (_url: string) => { + rewrite: (__url: string | URL) => { check(); + + const _url = typeof __url === 'string' ? __url : __url.toString(); const fixedURL = _url.replace(/([^:])\/{2,}/g, '$1/'); if (_url !== fixedURL) { console.warn(`Rewrite URL ${_url} is invalid, fixing to ${fixedURL}`); diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 01518f577cf..285ca3c175e 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -207,7 +207,7 @@ export interface RequestEventCommon * URL to rewrite to. When called, the response will immediately end with the correct rewrite url. * URL will remain unchanged in the browser history. */ - readonly rewrite: (url: string) => RewriteMessage; + readonly rewrite: (url: string | URL) => RewriteMessage; /** * When called, the response will immediately end with the given status code. This could be useful From eda65f4431a2f9fb4028e709d32f5b258f9edeca Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sat, 3 May 2025 23:58:36 +0300 Subject: [PATCH 13/37] minor rename --- .../src/middleware/request-handler/request-event.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index addca80b063..ab13c119e97 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -229,13 +229,13 @@ export function createRequestEvent( return new RedirectMessage(); }, - rewrite: (__url: string | URL) => { + rewrite: (_rewriteUrl: string | URL) => { check(); - const _url = typeof __url === 'string' ? __url : __url.toString(); - const fixedURL = _url.replace(/([^:])\/{2,}/g, '$1/'); - if (_url !== fixedURL) { - console.warn(`Rewrite URL ${_url} is invalid, fixing to ${fixedURL}`); + const rewriteUrl = typeof _rewriteUrl === 'string' ? _rewriteUrl : _rewriteUrl.toString(); + const fixedURL = rewriteUrl.replace(/([^:])\/{2,}/g, '$1/'); + if (rewriteUrl !== fixedURL) { + console.warn(`Rewrite URL ${rewriteUrl} is invalid, fixing to ${fixedURL}`); } url.pathname = fixedURL; headers.set('Rewrite-Location', fixedURL); From 3a108f6e82d82948f693ce6a7179b95da9a01793 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 06:47:27 +0300 Subject: [PATCH 14/37] properly handle urls+pathnames+searchparams --- .../request-handler/request-event.ts | 33 +++++++++++++++---- .../src/middleware/request-handler/types.ts | 8 +++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index ab13c119e97..21ee29207bc 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -229,16 +229,35 @@ export function createRequestEvent( return new RedirectMessage(); }, - rewrite: (_rewriteUrl: string | URL) => { + rewrite: (_rewriteUrl: string | URL, keepCurrentSearchParams = true) => { check(); + let rewriteUrl: URL; + if (typeof _rewriteUrl === 'string') { + rewriteUrl = new URL(_rewriteUrl, _rewriteUrl.startsWith('http') ? undefined : url); + } else { + rewriteUrl = _rewriteUrl; + } - const rewriteUrl = typeof _rewriteUrl === 'string' ? _rewriteUrl : _rewriteUrl.toString(); - const fixedURL = rewriteUrl.replace(/([^:])\/{2,}/g, '$1/'); - if (rewriteUrl !== fixedURL) { - console.warn(`Rewrite URL ${rewriteUrl} is invalid, fixing to ${fixedURL}`); + // Fix consecutive slashes - e.g //path//to//page -> /path/to/page + const fixedPathname = rewriteUrl.pathname.replace(/(^|[^:])\/{2,}/g, '$1/'); + if (rewriteUrl.pathname !== fixedPathname) { + console.warn(`Rewrite URL ${rewriteUrl.pathname} is invalid, fixing to ${fixedPathname}`); + rewriteUrl.pathname = fixedPathname; } - url.pathname = fixedURL; - headers.set('Rewrite-Location', fixedURL); + + if (keepCurrentSearchParams) { + url.searchParams.forEach((value, key) => { + // rewriteUrl values should take precedence over current url values + if (!rewriteUrl.searchParams.has(key)) { + rewriteUrl.searchParams.set(key, value); + } + }); + } + + url.pathname = rewriteUrl.pathname; + url.search = rewriteUrl.search; + + headers.set('Rewrite-Location', rewriteUrl.pathname); // should be restarted! routeModuleIndex = -1; return new RewriteMessage(); diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 285ca3c175e..9135059ef8a 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -204,10 +204,14 @@ export interface RequestEventCommon readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; /** - * URL to rewrite to. When called, the response will immediately end with the correct rewrite url. + * URL to rewrite to. When called, the flow will be reset to the new url. + * * URL will remain unchanged in the browser history. + * + * @param url - The url to rewrite to. + * @param keepCurrentSearchParams - Whether to keep the current search params. */ - readonly rewrite: (url: string | URL) => RewriteMessage; + readonly rewrite: (url: string | URL, keepCurrentSearchParams?: boolean) => RewriteMessage; /** * When called, the response will immediately end with the given status code. This could be useful From 2fa438a1383001f144148d279d9f2b57b8a76f8f Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 07:56:04 +0300 Subject: [PATCH 15/37] more readable loadedRoute handling. fixed params assignement issue --- .../middleware/request-handler/request-event.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 21ee29207bc..b9a90f58ff9 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -70,12 +70,8 @@ export function createRequestEvent( const next = async (_loadedRoute = loadedRoute, _requestHandlers = requestHandlers) => { routeModuleIndex++; - // Replace loadedRoute with _loadedRoute incase of a rewrite. if (loadedRoute !== _loadedRoute && _loadedRoute !== null && loadedRoute !== null) { - for (let i = 0; i < _loadedRoute.length; i++) { - loadedRoute[i] = _loadedRoute[i]; - } - loadedRoute.splice(_loadedRoute.length); + loadedRoute = _loadedRoute; } while (routeModuleIndex < _requestHandlers.length) { @@ -142,14 +138,18 @@ export function createRequestEvent( [RequestEvLoaders]: loaders, [RequestEvMode]: serverRequestEv.mode, [RequestEvTrailingSlash]: trailingSlash, - [RequestEvRoute]: loadedRoute, + get [RequestEvRoute]() { + return loadedRoute; + }, [RequestEvQwikSerializer]: qwikSerializer, cookie, headers, env, method: request.method, signal: request.signal, - params: loadedRoute?.[1] ?? {}, + get params() { + return loadedRoute?.[1] ?? {}; + }, pathname: url.pathname, platform, query: url.searchParams, From d2a447a8cd0a98e646fb656c8d0118eb62b30c86 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 08:13:14 +0300 Subject: [PATCH 16/37] fix some types. WIP e2e tests - for some reason not working due to pathname missmatch --- .../api.json | 18 ++++++++- .../index.mdx | 31 ++++++++++++++- .../middleware.request-handler.api.md | 8 +++- .../request-handler/rewrite-handler.ts | 3 +- .../routes/(common)/products/[id]/index.tsx | 38 ++++++++++++++++++- starters/e2e/qwikcity/page.spec.ts | 26 +++++++++++++ 6 files changed, 117 insertions(+), 7 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index f1900fb37ba..9b3052d02b4 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -285,7 +285,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEvent extends RequestEventCommon \n```\n**Extends:** [RequestEventCommon](#requesteventcommon)<PLATFORM>\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[exited](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if the middleware chain has finished executing.\n\n\n
\n\n[getWritableStream](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => WritableStream<Uint8Array>\n\n\n\n\nLow-level access to write to the HTTP response stream. Once `getWritableStream()` is called, the status and headers can no longer be modified and will be sent over the network.\n\n\n
\n\n[headersSent](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if headers have been sent, preventing any more headers from being set.\n\n\n
\n\n[next](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<void>\n\n\n\n\nInvoke the next middleware function in the chain.\n\nNOTE: Ensure that the call to `next()` is `await`ed.\n\n\n
", + "content": "```typescript\nexport interface RequestEvent extends RequestEventCommon \n```\n**Extends:** [RequestEventCommon](#requesteventcommon)<PLATFORM>\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[exited](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if the middleware chain has finished executing.\n\n\n
\n\n[getWritableStream](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => WritableStream<Uint8Array>\n\n\n\n\nLow-level access to write to the HTTP response stream. Once `getWritableStream()` is called, the status and headers can no longer be modified and will be sent over the network.\n\n\n
\n\n[headersSent](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if headers have been sent, preventing any more headers from being set.\n\n\n
\n\n[next](#)\n\n\n\n\n`readonly`\n\n\n\n\n(loadedRoute?: LoadedRoute \\| null, requestHandlers?: [RequestHandler](#requesthandler)<any>\\[\\]) => Promise<void>\n\n\n\n\nInvoke the next middleware function in the chain.\n\nNOTE: Ensure that the call to `next()` is `await`ed.\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requestevent.md" }, @@ -327,7 +327,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
\n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
\n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
\n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
\n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
\n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
\n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
\n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
\n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
", + "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
\n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
\n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
\n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
\n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
\n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
\n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(url: string \\| URL, keepCurrentSearchParams?: boolean) => [RewriteMessage](#rewritemessage)\n\n\n\n\nURL to rewrite to. When called, the flow will be reset to the new url.\n\nURL will remain unchanged in the browser history.\n\n\n
\n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
\n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
\n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventcommon.md" }, @@ -401,6 +401,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.resolvevalue.md" }, + { + "name": "RewriteMessage", + "id": "rewritemessage", + "hierarchy": [ + { + "name": "RewriteMessage", + "id": "rewritemessage" + } + ], + "kind": "Class", + "content": "```typescript\nexport declare class RewriteMessage extends AbortMessage \n```\n**Extends:** [AbortMessage](#abortmessage)", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts", + "mdFile": "qwik-city.rewritemessage.md" + }, { "name": "ServerError", "id": "servererror", diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index acf25004244..c430d9d8770 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -813,7 +813,7 @@ True if headers have been sent, preventing any more headers from being set. -() => Promise<void> +(loadedRoute?: LoadedRoute \| null, requestHandlers?: [RequestHandler](#requesthandler)<any>[]) => Promise<void> @@ -1312,6 +1312,25 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections +[rewrite](#) + + + +`readonly` + + + +(url: string \| URL, keepCurrentSearchParams?: boolean) => [RewriteMessage](#rewritemessage) + + + +URL to rewrite to. When called, the flow will be reset to the new url. + +URL will remain unchanged in the browser history. + + + + [send](#) @@ -1462,6 +1481,16 @@ export interface ResolveValue [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts) +## RewriteMessage + +```typescript +export declare class RewriteMessage extends AbortMessage +``` + +**Extends:** [AbortMessage](#abortmessage) + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts) + ## ServerError ```typescript diff --git a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md index 0fde166fead..94ae0e6e4cb 100644 --- a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md @@ -93,7 +93,8 @@ export interface RequestEvent extends RequestEventC readonly exited: boolean; readonly getWritableStream: () => WritableStream; readonly headersSent: boolean; - readonly next: () => Promise; + // Warning: (ae-forgotten-export) The symbol "LoadedRoute" needs to be exported by the entry point index.d.ts + readonly next: (loadedRoute?: LoadedRoute | null, requestHandlers?: RequestHandler[]) => Promise; } // @public (undocumented) @@ -134,6 +135,7 @@ export interface RequestEventCommon extends Request readonly locale: (local?: string) => string; // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; + readonly rewrite: (url: string | URL, keepCurrentSearchParams?: boolean) => RewriteMessage; // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts readonly send: SendMethod; // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts @@ -174,6 +176,10 @@ export interface ResolveValue { (action: Action): Promise; } +// @public (undocumented) +export class RewriteMessage extends AbortMessage { +} + // @public (undocumented) export class ServerError extends Error { constructor(status: number, data: T); diff --git a/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts b/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts index 77328eb3c1c..eb65b547a12 100644 --- a/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts @@ -1,5 +1,4 @@ -/** @public */ -export class AbortMessage {} +import { AbortMessage } from './redirect-handler'; /** @public */ export class RewriteMessage extends AbortMessage {} diff --git a/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx b/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx index 9b34a0b20e4..15416ba4ef2 100644 --- a/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx +++ b/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx @@ -68,6 +68,22 @@ export default component$(() => { T-Shirt (Redirect to /products/tshirt) +
  • + + T-Shirt (Rewrite to /products/tshirt) + +
  • +
  • + + T-Shirt (Rewrite to /products/tshirt) + +
  • = { }; export const useProductLoader = routeLoader$( - async ({ headers, json, error, params, query, redirect, status }) => { + async ({ + headers, + json, + error, + params, + query, + redirect, + rewrite, + status, + url, + }) => { // Serverside Endpoint // During SSR, this method is called directly on the server and returns the data object // On the client, this same data can be requested with fetch() at the same URL, but also @@ -128,6 +154,16 @@ export const useProductLoader = routeLoader$( throw redirect(301, "/qwikcity-test/products/tshirt/"); } + if (id === "shirt-rewrite-plain_string") { + // Rewrite, which should have the same effect as redirect but with keep the url + throw rewrite("/qwikcity-test/products/tshirt/"); + } + + if (id === "shirt-rewrite-url_object") { + // Rewrite, which should have the same effect as redirect but with keep the url + throw rewrite(new URL("/qwikcity-test/products/tshirt/", url.origin)); + } + if (id === "error") { throw error(500, "Error from server"); } diff --git a/starters/e2e/qwikcity/page.spec.ts b/starters/e2e/qwikcity/page.spec.ts index 1ae8164c37f..c163a4ef1f9 100644 --- a/starters/e2e/qwikcity/page.spec.ts +++ b/starters/e2e/qwikcity/page.spec.ts @@ -108,6 +108,32 @@ function tests() { h1: "Product: tshirt", activeHeaderLink: "Products", }); + + /*********** Products: shirt (rewrite to /products/tshirt) ***********/ + await linkNavigate( + ctx, + '[data-test-link="products-shirt-rewrite-plain_string"]', + ); + await assertPage(ctx, { + pathname: "/qwikcity-test/products/shirt-rewrite-plain_string/", + title: "Product tshirt - Qwik", + layoutHierarchy: ["root"], + h1: "Product: tshirt", + activeHeaderLink: "Products", + }); + + /*********** Products: shirt (rewrite to /products/tshirt) ***********/ + await linkNavigate( + ctx, + '[data-test-link="products-shirt-rewrite-url_object"]', + ); + await assertPage(ctx, { + pathname: "/qwikcity-test/products/shirt-rewrite-url_object/", + title: "Product tshirt - Qwik", + layoutHierarchy: ["root"], + h1: "Product: tshirt", + activeHeaderLink: "Products", + }); } /*********** Products: hoodie (404) ***********/ From 6ee2b255b16a468da95c540a76d0561c3dff3643 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 08:38:33 +0300 Subject: [PATCH 17/37] attempt to fix the rewrite in preview - no luck --- .../src/middleware/request-handler/request-handler.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-handler.ts b/packages/qwik-city/src/middleware/request-handler/request-handler.ts index 4bb17fc0e13..4356fae7abc 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-handler.ts @@ -26,18 +26,24 @@ export async function requestHandler( render ); + console.log('routeAndHandlers', routeAndHandlers); + if (routeAndHandlers) { const [route, requestHandlers] = routeAndHandlers; const applyRewrite: ApplyRewriteInternal = async (url: URL) => { + console.log('applyRewrite__url', url); + const matchPathname = getRouteMatchPathname(url.pathname, qwikCityPlan.trailingSlash); const routeAndHandlers = await loadRequestHandlers( qwikCityPlan, - url.pathname, + matchPathname, serverRequestEv.request.method, checkOrigin ?? true, render ); + console.log('applyRewrite__routeAndHandlers', routeAndHandlers); + if (routeAndHandlers) { const [loadedRoute, requestHandlers] = routeAndHandlers; return { loadedRoute, requestHandlers }; From 94eedb9486d1e88bb4f9ecf84694f3b8f22fc416 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 08:45:13 +0300 Subject: [PATCH 18/37] texts --- .../src/middleware/request-handler/resolve-request-handlers.ts | 1 - packages/qwik-city/src/middleware/request-handler/types.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index e8d4f248f04..6a9f6159c24 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -79,7 +79,6 @@ export const resolveRequestHandlers = ( } const routeModules = route[2]; requestHandlers.push(handleRedirect); - requestHandlers.push(handleRewrite); _resolveRequestHandlers( routeLoaders, diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 9135059ef8a..4d0ad254eea 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -204,7 +204,7 @@ export interface RequestEventCommon readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; /** - * URL to rewrite to. When called, the flow will be reset to the new url. + * URL to rewrite to. When called, the flow will reset to display the given url route. * * URL will remain unchanged in the browser history. * From 83400041f802514ab9df0bcdf01a0064cd9980aa Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 12:20:01 +0300 Subject: [PATCH 19/37] fix preview whitescreen. --- .../qwik-city/src/middleware/request-handler/request-event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index b9a90f58ff9..7ac19a97dee 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -70,7 +70,7 @@ export function createRequestEvent( const next = async (_loadedRoute = loadedRoute, _requestHandlers = requestHandlers) => { routeModuleIndex++; - if (loadedRoute !== _loadedRoute && _loadedRoute !== null && loadedRoute !== null) { + if (loadedRoute !== _loadedRoute) { loadedRoute = _loadedRoute; } From 69daa0b7a7a4abc43293026a099f997ddf7c7fda Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 17:30:56 +0300 Subject: [PATCH 20/37] introduce canonicalUrl - to know the request's original url --- .../src/middleware/request-handler/request-event.ts | 9 +++++++-- .../request-handler/resolve-request-handlers.ts | 7 ++++--- .../src/middleware/request-handler/response-page.ts | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 7ac19a97dee..a103fb3a3ac 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -147,12 +147,17 @@ export function createRequestEvent( env, method: request.method, signal: request.signal, + canonicalUrl: new URL(url), get params() { return loadedRoute?.[1] ?? {}; }, - pathname: url.pathname, + get pathname() { + return url.pathname; + }, platform, - query: url.searchParams, + get query() { + return url.searchParams; + }, request, url, basePathname, diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 6a9f6159c24..b9782e09a8f 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -359,7 +359,8 @@ async function pureServerFunction(ev: RequestEvent) { function fixTrailingSlash(ev: RequestEvent) { const trailingSlash = getRequestTrailingSlash(ev); - const { basePathname, pathname, url, sharedMap } = ev; + const { basePathname, canonicalUrl, sharedMap } = ev; + const { pathname, search } = canonicalUrl; const isQData = sharedMap.has(IsQData); if (!isQData && pathname !== basePathname && !pathname.endsWith('.html')) { // only check for slash redirect on pages @@ -367,7 +368,7 @@ function fixTrailingSlash(ev: RequestEvent) { // must have a trailing slash if (!pathname.endsWith('/')) { // add slash to existing pathname - throw ev.redirect(HttpStatus.MovedPermanently, pathname + '/' + url.search); + throw ev.redirect(HttpStatus.MovedPermanently, pathname + '/' + search); } } else { // should not have a trailing slash @@ -375,7 +376,7 @@ function fixTrailingSlash(ev: RequestEvent) { // remove slash from existing pathname throw ev.redirect( HttpStatus.MovedPermanently, - pathname.slice(0, pathname.length - 1) + url.search + pathname.slice(0, pathname.length - 1) + search ); } } diff --git a/packages/qwik-city/src/middleware/request-handler/response-page.ts b/packages/qwik-city/src/middleware/request-handler/response-page.ts index 00981720910..0be3099a785 100644 --- a/packages/qwik-city/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-city/src/middleware/request-handler/response-page.ts @@ -10,7 +10,7 @@ import { } from './request-event'; export function getQwikCityServerData(requestEv: RequestEvent) { - const { url, params, request, status, locale } = requestEv; + const { params, request, status, locale, canonicalUrl } = requestEv; const requestHeaders: Record = {}; request.headers.forEach((value, key) => (requestHeaders[key] = value)); @@ -19,7 +19,7 @@ export function getQwikCityServerData(requestEv: RequestEvent) { const routeName = requestEv.sharedMap.get(RequestRouteName) as string; const nonce = requestEv.sharedMap.get(RequestEvSharedNonce); const headers = requestEv.request.headers; - const reconstructedUrl = new URL(url.pathname + url.search, url); + const reconstructedUrl = new URL(canonicalUrl.pathname + canonicalUrl.search, canonicalUrl); const host = headers.get('X-Forwarded-Host')!; const protocol = headers.get('X-Forwarded-Proto')!; if (host) { From 09765f3cd2bd4c313664fc1abf72710be330718a Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 18:07:06 +0300 Subject: [PATCH 21/37] everything works including e2e's. will write some more and we're good to go --- .../api.json | 4 +-- .../index.mdx | 25 ++++++++++++++++++- .../middleware.request-handler.api.md | 1 + .../src/middleware/request-handler/types.ts | 13 ++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index 9b3052d02b4..e604f2c0b53 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -313,7 +313,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventBase \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[basePathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nThe base pathname of the request, which can be configured at build time. Defaults to `/`.\n\n\n
    \n\n[cacheControl](#)\n\n\n\n\n`readonly`\n\n\n\n\n(cacheControl: [CacheControl](#cachecontrol), target?: CacheControlTarget) => void\n\n\n\n\nConvenience method to set the Cache-Control header. Depending on your CDN, you may want to add another cacheControl with the second argument set to `CDN-Cache-Control` or any other value (we provide the most common values for auto-complete, but you can use any string you want).\n\nSee https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and https://qwik.dev/docs/caching/\\#CDN-Cache-Controls for more information.\n\n\n
    \n\n[clientConn](#)\n\n\n\n\n`readonly`\n\n\n\n\n[ClientConn](#clientconn)\n\n\n\n\nProvides information about the client connection, such as the IP address and the country the request originated from.\n\n\n
    \n\n[cookie](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Cookie](#cookie)\n\n\n\n\nHTTP request and response cookie. Use the `get()` method to retrieve a request cookie value. Use the `set()` method to set a response cookie value.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n\n\n
    \n\n[env](#)\n\n\n\n\n`readonly`\n\n\n\n\n[EnvGetter](#envgetter)\n\n\n\n\nPlatform provided environment variables.\n\n\n
    \n\n[headers](#)\n\n\n\n\n`readonly`\n\n\n\n\nHeaders\n\n\n\n\nHTTP response headers. Notice it will be empty until you first add a header. If you want to read the request headers, use `request.headers` instead.\n\nhttps://developer.mozilla.org/en-US/docs/Glossary/Response\\_header\n\n\n
    \n\n[method](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nHTTP request method.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\n\n
    \n\n[params](#)\n\n\n\n\n`readonly`\n\n\n\n\nReadonly<Record<string, string>>\n\n\n\n\nURL path params which have been parsed from the current url pathname segments. Use `query` to instead retrieve the query string search params.\n\n\n
    \n\n[parseBody](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<unknown>\n\n\n\n\nThis method will check the request headers for a `Content-Type` header and parse the body accordingly. It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.\n\nIf the `Content-Type` header is not set, it will return `null`.\n\n\n
    \n\n[pathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nURL pathname. Does not include the protocol, domain, query string (search params) or hash.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URL/pathname\n\n\n
    \n\n[platform](#)\n\n\n\n\n`readonly`\n\n\n\n\nPLATFORM\n\n\n\n\nPlatform specific data and functions\n\n\n
    \n\n[query](#)\n\n\n\n\n`readonly`\n\n\n\n\nURLSearchParams\n\n\n\n\nURL Query Strings (URL Search Params). Use `params` to instead retrieve the route params found in the url pathname.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n\n\n
    \n\n[request](#)\n\n\n\n\n`readonly`\n\n\n\n\nRequest\n\n\n\n\nHTTP request information.\n\n\n
    \n\n[sharedMap](#)\n\n\n\n\n`readonly`\n\n\n\n\nMap<string, any>\n\n\n\n\nShared Map across all the request handlers. Every HTTP request will get a new instance of the shared map. The shared map is useful for sharing data between request handlers.\n\n\n
    \n\n[signal](#)\n\n\n\n\n`readonly`\n\n\n\n\nAbortSignal\n\n\n\n\nRequest's AbortSignal (same as `request.signal`). This signal indicates that the request has been aborted.\n\n\n
    \n\n[url](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request URL.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEventBase \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[basePathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nThe base pathname of the request, which can be configured at build time. Defaults to `/`.\n\n\n
    \n\n[cacheControl](#)\n\n\n\n\n`readonly`\n\n\n\n\n(cacheControl: [CacheControl](#cachecontrol), target?: CacheControlTarget) => void\n\n\n\n\nConvenience method to set the Cache-Control header. Depending on your CDN, you may want to add another cacheControl with the second argument set to `CDN-Cache-Control` or any other value (we provide the most common values for auto-complete, but you can use any string you want).\n\nSee https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and https://qwik.dev/docs/caching/\\#CDN-Cache-Controls for more information.\n\n\n
    \n\n[canonicalUrl](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request Canonical URL.\n\nThis property was introduced to support the rewrite feature.\n\nIf rewrite is called, the canonicalUrl will remain as the original url, and the url property will be changed to the rewritten url.\n\nIf rewrite is never called as part of the request, the url property and the canonicalUrl are considered equal.\n\n\n
    \n\n[clientConn](#)\n\n\n\n\n`readonly`\n\n\n\n\n[ClientConn](#clientconn)\n\n\n\n\nProvides information about the client connection, such as the IP address and the country the request originated from.\n\n\n
    \n\n[cookie](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Cookie](#cookie)\n\n\n\n\nHTTP request and response cookie. Use the `get()` method to retrieve a request cookie value. Use the `set()` method to set a response cookie value.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n\n\n
    \n\n[env](#)\n\n\n\n\n`readonly`\n\n\n\n\n[EnvGetter](#envgetter)\n\n\n\n\nPlatform provided environment variables.\n\n\n
    \n\n[headers](#)\n\n\n\n\n`readonly`\n\n\n\n\nHeaders\n\n\n\n\nHTTP response headers. Notice it will be empty until you first add a header. If you want to read the request headers, use `request.headers` instead.\n\nhttps://developer.mozilla.org/en-US/docs/Glossary/Response\\_header\n\n\n
    \n\n[method](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nHTTP request method.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\n\n
    \n\n[params](#)\n\n\n\n\n`readonly`\n\n\n\n\nReadonly<Record<string, string>>\n\n\n\n\nURL path params which have been parsed from the current url pathname segments. Use `query` to instead retrieve the query string search params.\n\n\n
    \n\n[parseBody](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<unknown>\n\n\n\n\nThis method will check the request headers for a `Content-Type` header and parse the body accordingly. It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.\n\nIf the `Content-Type` header is not set, it will return `null`.\n\n\n
    \n\n[pathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nURL pathname. Does not include the protocol, domain, query string (search params) or hash.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URL/pathname\n\n\n
    \n\n[platform](#)\n\n\n\n\n`readonly`\n\n\n\n\nPLATFORM\n\n\n\n\nPlatform specific data and functions\n\n\n
    \n\n[query](#)\n\n\n\n\n`readonly`\n\n\n\n\nURLSearchParams\n\n\n\n\nURL Query Strings (URL Search Params). Use `params` to instead retrieve the route params found in the url pathname.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n\n\n
    \n\n[request](#)\n\n\n\n\n`readonly`\n\n\n\n\nRequest\n\n\n\n\nHTTP request information.\n\n\n
    \n\n[sharedMap](#)\n\n\n\n\n`readonly`\n\n\n\n\nMap<string, any>\n\n\n\n\nShared Map across all the request handlers. Every HTTP request will get a new instance of the shared map. The shared map is useful for sharing data between request handlers.\n\n\n
    \n\n[signal](#)\n\n\n\n\n`readonly`\n\n\n\n\nAbortSignal\n\n\n\n\nRequest's AbortSignal (same as `request.signal`). This signal indicates that the request has been aborted.\n\n\n
    \n\n[url](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request URL.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventbase.md" }, @@ -327,7 +327,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
    \n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
    \n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
    \n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
    \n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
    \n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
    \n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(url: string \\| URL, keepCurrentSearchParams?: boolean) => [RewriteMessage](#rewritemessage)\n\n\n\n\nURL to rewrite to. When called, the flow will be reset to the new url.\n\nURL will remain unchanged in the browser history.\n\n\n
    \n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
    \n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
    \n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
    \n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
    \n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
    \n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
    \n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
    \n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
    \n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(url: string \\| URL, keepCurrentSearchParams?: boolean) => [RewriteMessage](#rewritemessage)\n\n\n\n\nURL to rewrite to. When called, the flow will reset to display the given url route.\n\nURL will remain unchanged in the browser history.\n\n\n
    \n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
    \n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
    \n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventcommon.md" }, diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index c430d9d8770..9c10ef3a853 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -929,6 +929,29 @@ See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and +[canonicalUrl](#) + + + +`readonly` + + + +URL + + + +HTTP request Canonical URL. + +This property was introduced to support the rewrite feature. + +If rewrite is called, the canonicalUrl will remain as the original url, and the url property will be changed to the rewritten url. + +If rewrite is never called as part of the request, the url property and the canonicalUrl are considered equal. + + + + [clientConn](#) @@ -1324,7 +1347,7 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections -URL to rewrite to. When called, the flow will be reset to the new url. +URL to rewrite to. When called, the flow will reset to display the given url route. URL will remain unchanged in the browser history. diff --git a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md index 94ae0e6e4cb..cb0691f4022 100644 --- a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md @@ -108,6 +108,7 @@ export interface RequestEventBase { readonly basePathname: string; // Warning: (ae-forgotten-export) The symbol "CacheControlTarget" needs to be exported by the entry point index.d.ts readonly cacheControl: (cacheControl: CacheControl, target?: CacheControlTarget) => void; + readonly canonicalUrl: URL; readonly clientConn: ClientConn; readonly cookie: Cookie; readonly env: EnvGetter; diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 4d0ad254eea..42b1288c7cd 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -299,6 +299,19 @@ export interface RequestEventBase { /** HTTP request URL. */ readonly url: URL; + /** + * HTTP request Canonical URL. + * + * This property was introduced to support the rewrite feature. + * + * If rewrite is called, the canonicalUrl will remain as the original url, and the url property + * will be changed to the rewritten url. + * + * If rewrite is never called as part of the request, the url property and the canonicalUrl are + * considered equal. + */ + readonly canonicalUrl: URL; + /** The base pathname of the request, which can be configured at build time. Defaults to `/`. */ readonly basePathname: string; From 9d610ff7fa337bbdf83ff22c3ee99883127876bf Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 18:57:54 +0300 Subject: [PATCH 22/37] everything works well. --- .../qwik-city-middleware-request-handler/api.json | 2 +- .../qwik-city-middleware-request-handler/index.mdx | 4 +++- .../middleware.request-handler.api.md | 2 +- .../src/middleware/request-handler/request-event.ts | 5 ++++- .../src/middleware/request-handler/types.ts | 6 ++++-- .../src/routes/(common)/products/[id]/index.tsx | 12 ++++++++++-- starters/e2e/qwikcity/page.spec.ts | 13 +++++++++++++ starters/e2e/qwikcity/util.ts | 11 +++++++++++ 8 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index e604f2c0b53..73f17b34bb1 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -327,7 +327,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
    \n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
    \n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
    \n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
    \n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
    \n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
    \n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(url: string \\| URL, keepCurrentSearchParams?: boolean) => [RewriteMessage](#rewritemessage)\n\n\n\n\nURL to rewrite to. When called, the flow will reset to display the given url route.\n\nURL will remain unchanged in the browser history.\n\n\n
    \n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
    \n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
    \n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
    \n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
    \n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
    \n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
    \n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
    \n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
    \n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(url: string \\| URL) => [RewriteMessage](#rewritemessage)\n\n\n\n\nURL to rewrite to. When called, the flow will reset to display the given url route.\n\nURL will remain unchanged in the browser history.\n\nNote that if the url is a string without a protocol, the rewritten url will inherit the search params from the canonical url.\n\n\n
    \n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
    \n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
    \n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventcommon.md" }, diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index 9c10ef3a853..50722bd6a56 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -1343,7 +1343,7 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections -(url: string \| URL, keepCurrentSearchParams?: boolean) => [RewriteMessage](#rewritemessage) +(url: string \| URL) => [RewriteMessage](#rewritemessage) @@ -1351,6 +1351,8 @@ URL to rewrite to. When called, the flow will reset to display the given url rou URL will remain unchanged in the browser history. +Note that if the url is a string without a protocol, the rewritten url will inherit the search params from the canonical url. + diff --git a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md index cb0691f4022..ebc83d9e490 100644 --- a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md @@ -136,7 +136,7 @@ export interface RequestEventCommon extends Request readonly locale: (local?: string) => string; // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; - readonly rewrite: (url: string | URL, keepCurrentSearchParams?: boolean) => RewriteMessage; + readonly rewrite: (url: string | URL) => RewriteMessage; // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts readonly send: SendMethod; // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index a103fb3a3ac..4afacbd3646 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -234,7 +234,7 @@ export function createRequestEvent( return new RedirectMessage(); }, - rewrite: (_rewriteUrl: string | URL, keepCurrentSearchParams = true) => { + rewrite: (_rewriteUrl: string | URL) => { check(); let rewriteUrl: URL; if (typeof _rewriteUrl === 'string') { @@ -250,6 +250,9 @@ export function createRequestEvent( rewriteUrl.pathname = fixedPathname; } + // Assume that if Devs passed a string without a protocol, they want to keep the current search + const keepCurrentSearchParams = + typeof _rewriteUrl === 'string' && !_rewriteUrl.startsWith('http'); if (keepCurrentSearchParams) { url.searchParams.forEach((value, key) => { // rewriteUrl values should take precedence over current url values diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 42b1288c7cd..35ce3f22d75 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -208,10 +208,12 @@ export interface RequestEventCommon * * URL will remain unchanged in the browser history. * + * Note that if the url is a string without a protocol, the rewritten url will inherit the search + * params from the canonical url. + * * @param url - The url to rewrite to. - * @param keepCurrentSearchParams - Whether to keep the current search params. */ - readonly rewrite: (url: string | URL, keepCurrentSearchParams?: boolean) => RewriteMessage; + readonly rewrite: (url: string | URL) => RewriteMessage; /** * When called, the response will immediately end with the given status code. This could be useful diff --git a/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx b/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx index 15416ba4ef2..7384e92fdf1 100644 --- a/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx +++ b/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx @@ -84,6 +84,15 @@ export default component$(() => { T-Shirt (Rewrite to /products/tshirt)
  • +
  • + + T-Shirt (Rewrite to /products/tshirt) Also trailing slash should be + added. + +
  • | "empty"; title?: string; h1?: string; layoutHierarchy?: string[]; From 4f899bda352762291d64a30cbd18625f3598dec2 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 19:09:49 +0300 Subject: [PATCH 23/37] bump --- .changeset/dirty-dolls-heal.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/dirty-dolls-heal.md diff --git a/.changeset/dirty-dolls-heal.md b/.changeset/dirty-dolls-heal.md new file mode 100644 index 00000000000..53187384c80 --- /dev/null +++ b/.changeset/dirty-dolls-heal.md @@ -0,0 +1,18 @@ +--- +'@builder.io/qwik-city': minor +--- + +FEAT: Support rewrite feature. should work like redirect, but without modifying the address bar url + +Example usage: +``` +export const onRequest: RequestHandler = async ({ url, rewrite }) => { + if (url.pathname.includes("/articles/the-best-article-in-the-world")) { + const artistId = db.getArticleByName("the-best-article-in-the-world"); + + // Url will remain /articles/the-best-article-in-the-world, but under the hood, + // will render /articles/${artistId} + throw rewrite(`/articles/${artistId}`); + } +}; +``` \ No newline at end of file From ed7c3b54d85dfc8d6f13c41a59b9b739aa0e90d8 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 19:23:28 +0300 Subject: [PATCH 24/37] docs --- packages/docs/src/routes/api/qwik/api.json | 2 +- packages/docs/src/routes/api/qwik/index.mdx | 2 +- .../docs/(qwikcity)/guides/rewrites/index.mdx | 48 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index b65816293a7..9f917de5b35 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -1774,7 +1774,7 @@ } ], "kind": "Function", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
    \n**Returns:**\n\n[JSXNode](#jsxnode)<'script'>", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
    \n**Returns:**\n\nJSXNode<'script'>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index ed080d84a66..5bdb160f491 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -3651,7 +3651,7 @@ opts **Returns:** -[JSXNode](#jsxnode)<'script'> +JSXNode<'script'> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts) diff --git a/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx new file mode 100644 index 00000000000..2d9e2218f51 --- /dev/null +++ b/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx @@ -0,0 +1,48 @@ +--- +title: Rewrites | Guides +description: Learn how to use rewrites in Qwik City. +contributors: + - omerman +updated_at: '2025-05-04T19:43:33Z' +created_at: '2025-05-04T23:45:13Z' +--- + +# Rewrites + +Sometimes you want to redirect a user from the current page to another page, +but you want to keep the current URL in the browser history. + +Let's say a user tries to access an article which is indexed by its name, +e.g `/articles/qwik-is-very-quick`. +but in our code, we access it by its id, on our directory structure. + +``` +src/routes/articles/ +├── [id] +├─── index.tsx +``` + + +```tsx title="src/routes/plugin@article-rewrite.tsx" +import type { RequestHandler } from "@builder.io/qwik-city"; + +export const onRequest: RequestHandler = async ({ url, rewrite }) => { + const pattern = /^\/articles\/(.*)$/; + // Detects /articles/, returns null if url does not match the pattern. + const match = url.pathname.match(pattern); + if (match) { + const articleName = match[1]; + const { id } = await db.getArticleByName(articleName); + throw rewrite(`/articles/${id}`); + } +}; +``` + +The `rewrite()` function, which was destructured in the RequestHandler function arguments, takes a pathname string or a URL object. + +```tsx +throw rewrite(`/articles/777/`); +throw rewrite(new URL(`/articles/777/`, url)); +``` + +Note: If you provide a relative path, Qwik City will rewrite the URL and apply search params from the original URL. From 3ab236ada0a83e4c8b54143578357b1901633db7 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 19:34:58 +0300 Subject: [PATCH 25/37] remove logs --- .../src/middleware/request-handler/request-handler.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-handler.ts b/packages/qwik-city/src/middleware/request-handler/request-handler.ts index 4356fae7abc..b8738331069 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-handler.ts @@ -26,13 +26,10 @@ export async function requestHandler( render ); - console.log('routeAndHandlers', routeAndHandlers); - if (routeAndHandlers) { const [route, requestHandlers] = routeAndHandlers; const applyRewrite: ApplyRewriteInternal = async (url: URL) => { - console.log('applyRewrite__url', url); const matchPathname = getRouteMatchPathname(url.pathname, qwikCityPlan.trailingSlash); const routeAndHandlers = await loadRequestHandlers( qwikCityPlan, @@ -42,8 +39,6 @@ export async function requestHandler( render ); - console.log('applyRewrite__routeAndHandlers', routeAndHandlers); - if (routeAndHandlers) { const [loadedRoute, requestHandlers] = routeAndHandlers; return { loadedRoute, requestHandlers }; From c35e5394a093620a76fe522713f3e0d175b78af8 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 19:36:24 +0300 Subject: [PATCH 26/37] minor simplification --- .../src/middleware/request-handler/request-event.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 4afacbd3646..cac7d8fc5b5 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -69,10 +69,7 @@ export function createRequestEvent( const next = async (_loadedRoute = loadedRoute, _requestHandlers = requestHandlers) => { routeModuleIndex++; - - if (loadedRoute !== _loadedRoute) { - loadedRoute = _loadedRoute; - } + loadedRoute = _loadedRoute; while (routeModuleIndex < _requestHandlers.length) { const moduleRequestHandler = _requestHandlers[routeModuleIndex]; From 09927c2c3f2821ca38cfa91af683ec3b6d7a4042 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Sun, 4 May 2025 19:39:23 +0300 Subject: [PATCH 27/37] more self pr --- .../request-handler/request-event.ts | 2 +- .../request-handler/user-response.ts | 28 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index cac7d8fc5b5..b7165385748 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -263,7 +263,7 @@ export function createRequestEvent( url.search = rewriteUrl.search; headers.set('Rewrite-Location', rewriteUrl.pathname); - // should be restarted! + // We need to restart the handlers chain, because the loadedRoute has changed. routeModuleIndex = -1; return new RewriteMessage(); }, diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 544003898cf..1aec9583bf5 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -74,27 +74,21 @@ async function runNext( rewriteAttempt = 1 ) { try { - try { - // Run all middlewares - await requestEv.next(loadedRoute, requestHandlers); - } catch (e) { - if (e instanceof RewriteMessage) { - if (rewriteAttempt > 5) { - throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); - } - - const url = new URL(requestEv.url); - url.pathname = requestEv.headers.get('Rewrite-Location')!; - const { loadedRoute, requestHandlers } = await applyRewrite(url); - return await _runNext(resolve, loadedRoute, requestHandlers, rewriteAttempt + 1); - } - - throw e; - } + // Run all middlewares + await requestEv.next(loadedRoute, requestHandlers); } catch (e) { if (e instanceof RedirectMessage) { const stream = requestEv.getWritableStream(); await stream.close(); + } else if (e instanceof RewriteMessage) { + if (rewriteAttempt > 5) { + throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); + } + + const url = new URL(requestEv.url); + url.pathname = requestEv.headers.get('Rewrite-Location')!; + const { loadedRoute, requestHandlers } = await applyRewrite(url); + return await _runNext(resolve, loadedRoute, requestHandlers, rewriteAttempt + 1); } else if (e instanceof ServerError) { if (!requestEv.headersSent) { const status = e.status as StatusCodes; From 57d34387c5be6db04ff73570d2167160460bc8f7 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Mon, 5 May 2025 21:31:00 +0300 Subject: [PATCH 28/37] fixed it all, finally. --- .../src/buildtime/vite/dev-server.ts | 6 +-- .../request-handler/request-event.ts | 44 ++++++++++++---- .../request-handler/request-handler.ts | 6 +-- .../resolve-request-handlers.ts | 5 +- .../src/middleware/request-handler/types.ts | 5 +- .../request-handler/user-response.ts | 51 +++++++++++-------- packages/qwik-city/src/runtime/src/types.ts | 2 +- 7 files changed, 75 insertions(+), 44 deletions(-) diff --git a/packages/qwik-city/src/buildtime/vite/dev-server.ts b/packages/qwik-city/src/buildtime/vite/dev-server.ts index c24ca130f7e..0d6d8fe12d9 100644 --- a/packages/qwik-city/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/src/buildtime/vite/dev-server.ts @@ -18,7 +18,7 @@ import { matchRoute } from '../../runtime/src/route-matcher'; import { getMenuLoader } from '../../runtime/src/routing'; import type { ActionInternal, - ApplyRewriteInternal, + RebuildRouteInfoInternal, ContentMenu, LoadedRoute, LoaderInternal, @@ -228,7 +228,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { await server.ssrLoadModule('@qwik-serializer'); const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable }; - const applyRewrite: ApplyRewriteInternal = async (url: URL) => { + const rebuildRouteInfo: RebuildRouteInfoInternal = async (url: URL) => { const { serverPlugins, loadedRoute } = await resolveRoute(routeModulePaths, url); const requestHandlers = resolveRequestHandlers( serverPlugins, @@ -248,7 +248,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { serverRequestEv, loadedRoute, requestHandlers, - applyRewrite, + rebuildRouteInfo, ctx.opts.trailingSlash, ctx.opts.basePathname, qwikSerializer diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index b7165385748..99da6b09c64 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -67,12 +67,11 @@ export function createRequestEvent( let locale = serverRequestEv.locale; let status = 200; - const next = async (_loadedRoute = loadedRoute, _requestHandlers = requestHandlers) => { + const next = async () => { routeModuleIndex++; - loadedRoute = _loadedRoute; - while (routeModuleIndex < _requestHandlers.length) { - const moduleRequestHandler = _requestHandlers[routeModuleIndex]; + while (routeModuleIndex < requestHandlers.length) { + const moduleRequestHandler = requestHandlers[routeModuleIndex]; const asyncStore = globalThis.qcAsyncRequestStore; const result = asyncStore?.run ? asyncStore.run(requestEv, moduleRequestHandler, requestEv) @@ -84,6 +83,19 @@ export function createRequestEvent( } }; + const replay = ( + _loadedRoute: LoadedRoute | null, + _requestHandlers: RequestHandler[], + _url = url, + _routeModuleIndex = -1 + ) => { + loadedRoute = _loadedRoute; + requestHandlers = _requestHandlers; + url.pathname = _url.pathname; + url.search = _url.search; + routeModuleIndex = _routeModuleIndex; + }; + const check = () => { if (writableStream !== null) { throw new Error('Response already sent'); @@ -171,6 +183,8 @@ export function createRequestEvent( next, + replay, + exit, cacheControl: (cacheControl: CacheControl, target: CacheControlTarget = 'Cache-Control') => { @@ -259,12 +273,7 @@ export function createRequestEvent( }); } - url.pathname = rewriteUrl.pathname; - url.search = rewriteUrl.search; - - headers.set('Rewrite-Location', rewriteUrl.pathname); - // We need to restart the handlers chain, because the loadedRoute has changed. - routeModuleIndex = -1; + headers.set('Rewrite-Location', rewriteUrl.toString()); return new RewriteMessage(); }, @@ -344,6 +353,21 @@ export interface RequestEventInternal extends RequestEvent, RequestEventLoader { * @returns `true`, if `getWritableStream()` has already been called. */ isDirty(): boolean; + + /** + * Replay the request handlers chain with new loadedRoute and requestHandlers. + * + * @param loadedRoute - The new loaded route. + * @param requestHandlers - The new request handlers. + * @param url - The new URL (defaults to the current URL). + * @param routeModuleIndex - The new route module index (defaults to -1, full replay). + */ + replay( + loadedRoute: LoadedRoute | null, + requestHandlers: RequestHandler[], + url?: URL, + routeModuleIndex?: number + ): void; } export function getRequestLoaders(requestEv: RequestEventCommon) { diff --git a/packages/qwik-city/src/middleware/request-handler/request-handler.ts b/packages/qwik-city/src/middleware/request-handler/request-handler.ts index b8738331069..661f6fdea97 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-handler.ts @@ -1,6 +1,6 @@ import type { Render } from '@builder.io/qwik/server'; import { loadRoute } from '../../runtime/src/routing'; -import type { ApplyRewriteInternal, QwikCityPlan } from '../../runtime/src/types'; +import type { RebuildRouteInfoInternal, QwikCityPlan } from '../../runtime/src/types'; import { renderQwikMiddleware, resolveRequestHandlers } from './resolve-request-handlers'; import type { QwikSerializer, ServerRenderOptions, ServerRequestEvent } from './types'; import { getRouteMatchPathname, runQwikCity, type QwikCityRun } from './user-response'; @@ -29,7 +29,7 @@ export async function requestHandler( if (routeAndHandlers) { const [route, requestHandlers] = routeAndHandlers; - const applyRewrite: ApplyRewriteInternal = async (url: URL) => { + const rebuildRouteInfo: RebuildRouteInfoInternal = async (url: URL) => { const matchPathname = getRouteMatchPathname(url.pathname, qwikCityPlan.trailingSlash); const routeAndHandlers = await loadRequestHandlers( qwikCityPlan, @@ -51,7 +51,7 @@ export async function requestHandler( serverRequestEv, route, requestHandlers, - applyRewrite, + rebuildRouteInfo, qwikCityPlan.trailingSlash, qwikCityPlan.basePathname, qwikSerializer diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index b9782e09a8f..ce477fdf6bd 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -97,6 +97,7 @@ export const resolveRequestHandlers = ( requestHandlers.push(renderHandler); } } + return requestHandlers; }; @@ -547,6 +548,7 @@ export async function handleRewrite(requestEv: RequestEvent) { throw err; } } + if (requestEv.headersSent) { return; } @@ -557,13 +559,12 @@ export async function handleRewrite(requestEv: RequestEvent) { if (isRewrite) { const adaptedLocation = makeQDataPath(location); + if (adaptedLocation) { requestEv.headers.set('Rewrite-Location', adaptedLocation); - requestEv.getWritableStream().close(); return; } else { requestEv.status(200); - requestEv.headers.delete('Rewrite-Location'); } } } diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 35ce3f22d75..65d04c35eb7 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -476,10 +476,7 @@ export interface RequestEvent extends RequestEventC * * NOTE: Ensure that the call to `next()` is `await`ed. */ - readonly next: ( - loadedRoute?: LoadedRoute | null, - requestHandlers?: RequestHandler[] - ) => Promise; + readonly next: () => Promise; } declare global { diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 1aec9583bf5..8426301bace 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -1,5 +1,5 @@ import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city'; -import type { ApplyRewriteInternal, LoadedRoute } from '../../runtime/src/types'; +import type { RebuildRouteInfoInternal, LoadedRoute } from '../../runtime/src/types'; import { ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import { @@ -36,7 +36,7 @@ export function runQwikCity( serverRequestEv: ServerRequestEvent, loadedRoute: LoadedRoute | null, requestHandlers: RequestHandler[], - applyRewrite: ApplyRewriteInternal, + rebuildRouteInfo: RebuildRouteInfoInternal, trailingSlash = true, basePathname = '/', qwikSerializer: QwikSerializer @@ -57,25 +57,25 @@ export function runQwikCity( response: responsePromise, requestEv, completion: asyncStore - ? asyncStore.run(requestEv, runNext, requestEv, applyRewrite, resolve!) - : runNext(requestEv, applyRewrite, resolve!), + ? asyncStore.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) + : runNext(requestEv, rebuildRouteInfo, resolve!), }; } async function runNext( requestEv: RequestEventInternal, - applyRewrite: ApplyRewriteInternal, + rebuildRouteInfo: RebuildRouteInfoInternal, resolve: (value: any) => void ) { - async function _runNext( - resolve: (value: any) => void, - loadedRoute?: LoadedRoute | null, - requestHandlers?: RequestHandler[], - rewriteAttempt = 1 - ) { + let rewriteAttempt = 1; + let didRewrite = false; + + async function _runNext() { + didRewrite = false; + try { // Run all middlewares - await requestEv.next(loadedRoute, requestHandlers); + await requestEv.next(); } catch (e) { if (e instanceof RedirectMessage) { const stream = requestEv.getWritableStream(); @@ -85,10 +85,7 @@ async function runNext( throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); } - const url = new URL(requestEv.url); - url.pathname = requestEv.headers.get('Rewrite-Location')!; - const { loadedRoute, requestHandlers } = await applyRewrite(url); - return await _runNext(resolve, loadedRoute, requestHandlers, rewriteAttempt + 1); + didRewrite = true; } else if (e instanceof ServerError) { if (!requestEv.headersSent) { const status = e.status as StatusCodes; @@ -122,15 +119,27 @@ async function runNext( } return e; } - } finally { - if (!requestEv.isDirty()) { - resolve(null); - } } + return undefined; } - return _runNext(resolve); + try { + let runResult = await _runNext(); + if (didRewrite) { + rewriteAttempt += 1; + const url = new URL(requestEv.headers.get('Rewrite-Location')!); + const { loadedRoute, requestHandlers } = await rebuildRouteInfo(url); + requestEv.replay(loadedRoute, requestHandlers, url); + runResult = await _runNext(); + } + + return runResult; + } finally { + if (!requestEv.isDirty()) { + resolve(null); + } + } } /** diff --git a/packages/qwik-city/src/runtime/src/types.ts b/packages/qwik-city/src/runtime/src/types.ts index b75e77b1c73..767a81db896 100644 --- a/packages/qwik-city/src/runtime/src/types.ts +++ b/packages/qwik-city/src/runtime/src/types.ts @@ -81,7 +81,7 @@ export type RouteStateInternal = { scroll?: boolean; }; -export type ApplyRewriteInternal = ( +export type RebuildRouteInfoInternal = ( url: URL ) => Promise<{ loadedRoute: LoadedRoute | null; requestHandlers: RequestHandler[] }>; From 4b7ff3a42f895928f2c639f6519d03daa0081392 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Mon, 5 May 2025 21:37:02 +0300 Subject: [PATCH 29/37] api updated --- .../api/qwik-city-middleware-request-handler/api.json | 2 +- .../api/qwik-city-middleware-request-handler/index.mdx | 2 +- packages/docs/src/routes/api/qwik/api.json | 2 +- packages/docs/src/routes/api/qwik/index.mdx | 2 +- .../request-handler/middleware.request-handler.api.md | 3 +-- packages/qwik-city/src/middleware/request-handler/types.ts | 7 +++---- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index 73f17b34bb1..76c472a1db2 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -285,7 +285,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEvent extends RequestEventCommon \n```\n**Extends:** [RequestEventCommon](#requesteventcommon)<PLATFORM>\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[exited](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if the middleware chain has finished executing.\n\n\n
    \n\n[getWritableStream](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => WritableStream<Uint8Array>\n\n\n\n\nLow-level access to write to the HTTP response stream. Once `getWritableStream()` is called, the status and headers can no longer be modified and will be sent over the network.\n\n\n
    \n\n[headersSent](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if headers have been sent, preventing any more headers from being set.\n\n\n
    \n\n[next](#)\n\n\n\n\n`readonly`\n\n\n\n\n(loadedRoute?: LoadedRoute \\| null, requestHandlers?: [RequestHandler](#requesthandler)<any>\\[\\]) => Promise<void>\n\n\n\n\nInvoke the next middleware function in the chain.\n\nNOTE: Ensure that the call to `next()` is `await`ed.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEvent extends RequestEventCommon \n```\n**Extends:** [RequestEventCommon](#requesteventcommon)<PLATFORM>\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[exited](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if the middleware chain has finished executing.\n\n\n
    \n\n[getWritableStream](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => WritableStream<Uint8Array>\n\n\n\n\nLow-level access to write to the HTTP response stream. Once `getWritableStream()` is called, the status and headers can no longer be modified and will be sent over the network.\n\n\n
    \n\n[headersSent](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\nTrue if headers have been sent, preventing any more headers from being set.\n\n\n
    \n\n[next](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<void>\n\n\n\n\nInvoke the next middleware function in the chain.\n\nNOTE: Ensure that the call to `next()` is `await`ed.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requestevent.md" }, diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index 50722bd6a56..576fe5bf765 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -813,7 +813,7 @@ True if headers have been sent, preventing any more headers from being set. -(loadedRoute?: LoadedRoute \| null, requestHandlers?: [RequestHandler](#requesthandler)<any>[]) => Promise<void> +() => Promise<void> diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 9f917de5b35..b65816293a7 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -1774,7 +1774,7 @@ } ], "kind": "Function", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
    \n**Returns:**\n\nJSXNode<'script'>", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
    \n**Returns:**\n\n[JSXNode](#jsxnode)<'script'>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index 5bdb160f491..ed080d84a66 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -3651,7 +3651,7 @@ opts **Returns:** -JSXNode<'script'> +[JSXNode](#jsxnode)<'script'> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts) diff --git a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md index ebc83d9e490..5bbb060e07b 100644 --- a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md @@ -93,8 +93,7 @@ export interface RequestEvent extends RequestEventC readonly exited: boolean; readonly getWritableStream: () => WritableStream; readonly headersSent: boolean; - // Warning: (ae-forgotten-export) The symbol "LoadedRoute" needs to be exported by the entry point index.d.ts - readonly next: (loadedRoute?: LoadedRoute | null, requestHandlers?: RequestHandler[]) => Promise; + readonly next: () => Promise; } // @public (undocumented) diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 65d04c35eb7..9fe060ff83a 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -1,11 +1,10 @@ +import type { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import type { Action, FailReturn, Loader, QwikCityPlan } from '@builder.io/qwik-city'; import type { Render, RenderOptions } from '@builder.io/qwik/server'; -import type { QwikCityPlan, FailReturn, Action, Loader } from '@builder.io/qwik-city'; +import type { ServerError } from './error-handler'; import type { AbortMessage, RedirectMessage } from './redirect-handler'; import type { RequestEventInternal } from './request-event'; -import type { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; -import type { ServerError } from './error-handler'; import type { RewriteMessage } from './rewrite-handler'; -import type { LoadedRoute } from '../../runtime/src/types'; /** @public */ export interface EnvGetter { From cd4e201dd6a141f086886449e698fc99278ac972 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 6 May 2025 07:55:20 +0300 Subject: [PATCH 30/37] CR fixes --- .changeset/dirty-dolls-heal.md | 5 +- .../api.json | 4 +- .../index.mdx | 54 +++++++++---------- .../middleware.request-handler.api.md | 4 +- .../request-handler/request-event.ts | 45 ++++++---------- .../resolve-request-handlers.ts | 4 +- .../request-handler/response-page.ts | 4 +- .../src/middleware/request-handler/types.ts | 21 ++++---- .../request-handler/user-response.ts | 2 +- .../routes/(common)/products/[id]/index.tsx | 29 ++++++---- starters/e2e/qwikcity/page.spec.ts | 37 +++++++++---- starters/e2e/qwikcity/util.ts | 10 ++++ 12 files changed, 118 insertions(+), 101 deletions(-) diff --git a/.changeset/dirty-dolls-heal.md b/.changeset/dirty-dolls-heal.md index 53187384c80..dd5e6a307ba 100644 --- a/.changeset/dirty-dolls-heal.md +++ b/.changeset/dirty-dolls-heal.md @@ -2,10 +2,11 @@ '@builder.io/qwik-city': minor --- -FEAT: Support rewrite feature. should work like redirect, but without modifying the address bar url +FEAT: Added rewrite() to the RequestEvent object. It works like redirect but does not change the URL, + think of it as an internal redirect. Example usage: -``` +```ts export const onRequest: RequestHandler = async ({ url, rewrite }) => { if (url.pathname.includes("/articles/the-best-article-in-the-world")) { const artistId = db.getArticleByName("the-best-article-in-the-world"); diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index 76c472a1db2..4ffe5d338e0 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -313,7 +313,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventBase \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[basePathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nThe base pathname of the request, which can be configured at build time. Defaults to `/`.\n\n\n
    \n\n[cacheControl](#)\n\n\n\n\n`readonly`\n\n\n\n\n(cacheControl: [CacheControl](#cachecontrol), target?: CacheControlTarget) => void\n\n\n\n\nConvenience method to set the Cache-Control header. Depending on your CDN, you may want to add another cacheControl with the second argument set to `CDN-Cache-Control` or any other value (we provide the most common values for auto-complete, but you can use any string you want).\n\nSee https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and https://qwik.dev/docs/caching/\\#CDN-Cache-Controls for more information.\n\n\n
    \n\n[canonicalUrl](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request Canonical URL.\n\nThis property was introduced to support the rewrite feature.\n\nIf rewrite is called, the canonicalUrl will remain as the original url, and the url property will be changed to the rewritten url.\n\nIf rewrite is never called as part of the request, the url property and the canonicalUrl are considered equal.\n\n\n
    \n\n[clientConn](#)\n\n\n\n\n`readonly`\n\n\n\n\n[ClientConn](#clientconn)\n\n\n\n\nProvides information about the client connection, such as the IP address and the country the request originated from.\n\n\n
    \n\n[cookie](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Cookie](#cookie)\n\n\n\n\nHTTP request and response cookie. Use the `get()` method to retrieve a request cookie value. Use the `set()` method to set a response cookie value.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n\n\n
    \n\n[env](#)\n\n\n\n\n`readonly`\n\n\n\n\n[EnvGetter](#envgetter)\n\n\n\n\nPlatform provided environment variables.\n\n\n
    \n\n[headers](#)\n\n\n\n\n`readonly`\n\n\n\n\nHeaders\n\n\n\n\nHTTP response headers. Notice it will be empty until you first add a header. If you want to read the request headers, use `request.headers` instead.\n\nhttps://developer.mozilla.org/en-US/docs/Glossary/Response\\_header\n\n\n
    \n\n[method](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nHTTP request method.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\n\n
    \n\n[params](#)\n\n\n\n\n`readonly`\n\n\n\n\nReadonly<Record<string, string>>\n\n\n\n\nURL path params which have been parsed from the current url pathname segments. Use `query` to instead retrieve the query string search params.\n\n\n
    \n\n[parseBody](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<unknown>\n\n\n\n\nThis method will check the request headers for a `Content-Type` header and parse the body accordingly. It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.\n\nIf the `Content-Type` header is not set, it will return `null`.\n\n\n
    \n\n[pathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nURL pathname. Does not include the protocol, domain, query string (search params) or hash.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URL/pathname\n\n\n
    \n\n[platform](#)\n\n\n\n\n`readonly`\n\n\n\n\nPLATFORM\n\n\n\n\nPlatform specific data and functions\n\n\n
    \n\n[query](#)\n\n\n\n\n`readonly`\n\n\n\n\nURLSearchParams\n\n\n\n\nURL Query Strings (URL Search Params). Use `params` to instead retrieve the route params found in the url pathname.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n\n\n
    \n\n[request](#)\n\n\n\n\n`readonly`\n\n\n\n\nRequest\n\n\n\n\nHTTP request information.\n\n\n
    \n\n[sharedMap](#)\n\n\n\n\n`readonly`\n\n\n\n\nMap<string, any>\n\n\n\n\nShared Map across all the request handlers. Every HTTP request will get a new instance of the shared map. The shared map is useful for sharing data between request handlers.\n\n\n
    \n\n[signal](#)\n\n\n\n\n`readonly`\n\n\n\n\nAbortSignal\n\n\n\n\nRequest's AbortSignal (same as `request.signal`). This signal indicates that the request has been aborted.\n\n\n
    \n\n[url](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request URL.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEventBase \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[basePathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nThe base pathname of the request, which can be configured at build time. Defaults to `/`.\n\n\n
    \n\n[cacheControl](#)\n\n\n\n\n`readonly`\n\n\n\n\n(cacheControl: [CacheControl](#cachecontrol), target?: CacheControlTarget) => void\n\n\n\n\nConvenience method to set the Cache-Control header. Depending on your CDN, you may want to add another cacheControl with the second argument set to `CDN-Cache-Control` or any other value (we provide the most common values for auto-complete, but you can use any string you want).\n\nSee https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and https://qwik.dev/docs/caching/\\#CDN-Cache-Controls for more information.\n\n\n
    \n\n[clientConn](#)\n\n\n\n\n`readonly`\n\n\n\n\n[ClientConn](#clientconn)\n\n\n\n\nProvides information about the client connection, such as the IP address and the country the request originated from.\n\n\n
    \n\n[cookie](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Cookie](#cookie)\n\n\n\n\nHTTP request and response cookie. Use the `get()` method to retrieve a request cookie value. Use the `set()` method to set a response cookie value.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n\n\n
    \n\n[env](#)\n\n\n\n\n`readonly`\n\n\n\n\n[EnvGetter](#envgetter)\n\n\n\n\nPlatform provided environment variables.\n\n\n
    \n\n[headers](#)\n\n\n\n\n`readonly`\n\n\n\n\nHeaders\n\n\n\n\nHTTP response headers. Notice it will be empty until you first add a header. If you want to read the request headers, use `request.headers` instead.\n\nhttps://developer.mozilla.org/en-US/docs/Glossary/Response\\_header\n\n\n
    \n\n[method](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nHTTP request method.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\n\n
    \n\n[originalUrl](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request Canonical URL.\n\nThis property was introduced to support the rewrite feature.\n\nIf rewrite is called, the url property will be changed to the rewritten url. while originalUrl will stay the same(e.g the url inserted to the address bar).\n\nIf rewrite is never called as part of the request, the url property and the originalUrl are equal.\n\n\n
    \n\n[params](#)\n\n\n\n\n`readonly`\n\n\n\n\nReadonly<Record<string, string>>\n\n\n\n\nURL path params which have been parsed from the current url pathname segments. Use `query` to instead retrieve the query string search params.\n\n\n
    \n\n[parseBody](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<unknown>\n\n\n\n\nThis method will check the request headers for a `Content-Type` header and parse the body accordingly. It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.\n\nIf the `Content-Type` header is not set, it will return `null`.\n\n\n
    \n\n[pathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nURL pathname. Does not include the protocol, domain, query string (search params) or hash.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URL/pathname\n\n\n
    \n\n[platform](#)\n\n\n\n\n`readonly`\n\n\n\n\nPLATFORM\n\n\n\n\nPlatform specific data and functions\n\n\n
    \n\n[query](#)\n\n\n\n\n`readonly`\n\n\n\n\nURLSearchParams\n\n\n\n\nURL Query Strings (URL Search Params). Use `params` to instead retrieve the route params found in the url pathname.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n\n\n
    \n\n[request](#)\n\n\n\n\n`readonly`\n\n\n\n\nRequest\n\n\n\n\nHTTP request information.\n\n\n
    \n\n[sharedMap](#)\n\n\n\n\n`readonly`\n\n\n\n\nMap<string, any>\n\n\n\n\nShared Map across all the request handlers. Every HTTP request will get a new instance of the shared map. The shared map is useful for sharing data between request handlers.\n\n\n
    \n\n[signal](#)\n\n\n\n\n`readonly`\n\n\n\n\nAbortSignal\n\n\n\n\nRequest's AbortSignal (same as `request.signal`). This signal indicates that the request has been aborted.\n\n\n
    \n\n[url](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request URL.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventbase.md" }, @@ -327,7 +327,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
    \n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
    \n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
    \n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
    \n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
    \n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
    \n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(url: string \\| URL) => [RewriteMessage](#rewritemessage)\n\n\n\n\nURL to rewrite to. When called, the flow will reset to display the given url route.\n\nURL will remain unchanged in the browser history.\n\nNote that if the url is a string without a protocol, the rewritten url will inherit the search params from the canonical url.\n\n\n
    \n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
    \n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
    \n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[error](#)\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
    \n\n[exit](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
    \n\n[html](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
    \n\n[json](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
    \n\n[locale](#)\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
    \n\n[redirect](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
    \n\n[rewrite](#)\n\n\n\n\n`readonly`\n\n\n\n\n(pathname: string) => [RewriteMessage](#rewritemessage)\n\n\n\n\nWhen called, qwik-city will execute the path's matching route flow.\n\nThe url in the browser will remain unchanged.\n\n\n
    \n\n[send](#)\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
    \n\n[status](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
    \n\n[text](#)\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventcommon.md" }, diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index 576fe5bf765..a4ac0f73c46 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -929,29 +929,6 @@ See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and -[canonicalUrl](#) - - - -`readonly` - - - -URL - - - -HTTP request Canonical URL. - -This property was introduced to support the rewrite feature. - -If rewrite is called, the canonicalUrl will remain as the original url, and the url property will be changed to the rewritten url. - -If rewrite is never called as part of the request, the url property and the canonicalUrl are considered equal. - - - - [clientConn](#) @@ -1043,6 +1020,29 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods +[originalUrl](#) + + + +`readonly` + + + +URL + + + +HTTP request Canonical URL. + +This property was introduced to support the rewrite feature. + +If rewrite is called, the url property will be changed to the rewritten url. while originalUrl will stay the same(e.g the url inserted to the address bar). + +If rewrite is never called as part of the request, the url property and the originalUrl are equal. + + + + [params](#) @@ -1343,15 +1343,13 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections -(url: string \| URL) => [RewriteMessage](#rewritemessage) +(pathname: string) => [RewriteMessage](#rewritemessage) -URL to rewrite to. When called, the flow will reset to display the given url route. - -URL will remain unchanged in the browser history. +When called, qwik-city will execute the path's matching route flow. -Note that if the url is a string without a protocol, the rewritten url will inherit the search params from the canonical url. +The url in the browser will remain unchanged. diff --git a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md index 5bbb060e07b..f475a4ab0c9 100644 --- a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md @@ -107,12 +107,12 @@ export interface RequestEventBase { readonly basePathname: string; // Warning: (ae-forgotten-export) The symbol "CacheControlTarget" needs to be exported by the entry point index.d.ts readonly cacheControl: (cacheControl: CacheControl, target?: CacheControlTarget) => void; - readonly canonicalUrl: URL; readonly clientConn: ClientConn; readonly cookie: Cookie; readonly env: EnvGetter; readonly headers: Headers; readonly method: string; + readonly originalUrl: URL; readonly params: Readonly>; readonly parseBody: () => Promise; readonly pathname: string; @@ -135,7 +135,7 @@ export interface RequestEventCommon extends Request readonly locale: (local?: string) => string; // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; - readonly rewrite: (url: string | URL) => RewriteMessage; + readonly rewrite: (pathname: string) => RewriteMessage; // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts readonly send: SendMethod; // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 99da6b09c64..22ae1cb65dd 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -83,17 +83,16 @@ export function createRequestEvent( } }; - const replay = ( + const resetRoute = ( _loadedRoute: LoadedRoute | null, _requestHandlers: RequestHandler[], - _url = url, - _routeModuleIndex = -1 + _url = url ) => { loadedRoute = _loadedRoute; requestHandlers = _requestHandlers; url.pathname = _url.pathname; url.search = _url.search; - routeModuleIndex = _routeModuleIndex; + routeModuleIndex = -1; }; const check = () => { @@ -156,7 +155,7 @@ export function createRequestEvent( env, method: request.method, signal: request.signal, - canonicalUrl: new URL(url), + originalUrl: new URL(url), get params() { return loadedRoute?.[1] ?? {}; }, @@ -183,7 +182,7 @@ export function createRequestEvent( next, - replay, + resetRoute, exit, @@ -245,15 +244,15 @@ export function createRequestEvent( return new RedirectMessage(); }, - rewrite: (_rewriteUrl: string | URL) => { + rewrite: (pathname: string) => { check(); - let rewriteUrl: URL; - if (typeof _rewriteUrl === 'string') { - rewriteUrl = new URL(_rewriteUrl, _rewriteUrl.startsWith('http') ? undefined : url); - } else { - rewriteUrl = _rewriteUrl; + if (pathname.startsWith('http')) { + throw new Error('Rewrite does not support absolute urls'); } + const rewriteUrl = new URL(url); + rewriteUrl.pathname = pathname; + // Fix consecutive slashes - e.g //path//to//page -> /path/to/page const fixedPathname = rewriteUrl.pathname.replace(/(^|[^:])\/{2,}/g, '$1/'); if (rewriteUrl.pathname !== fixedPathname) { @@ -261,18 +260,6 @@ export function createRequestEvent( rewriteUrl.pathname = fixedPathname; } - // Assume that if Devs passed a string without a protocol, they want to keep the current search - const keepCurrentSearchParams = - typeof _rewriteUrl === 'string' && !_rewriteUrl.startsWith('http'); - if (keepCurrentSearchParams) { - url.searchParams.forEach((value, key) => { - // rewriteUrl values should take precedence over current url values - if (!rewriteUrl.searchParams.has(key)) { - rewriteUrl.searchParams.set(key, value); - } - }); - } - headers.set('Rewrite-Location', rewriteUrl.toString()); return new RewriteMessage(); }, @@ -355,18 +342,16 @@ export interface RequestEventInternal extends RequestEvent, RequestEventLoader { isDirty(): boolean; /** - * Replay the request handlers chain with new loadedRoute and requestHandlers. + * Reset the request event to the given route data. * * @param loadedRoute - The new loaded route. * @param requestHandlers - The new request handlers. - * @param url - The new URL (defaults to the current URL). - * @param routeModuleIndex - The new route module index (defaults to -1, full replay). + * @param url - The new URL of the route. */ - replay( + resetRoute( loadedRoute: LoadedRoute | null, requestHandlers: RequestHandler[], - url?: URL, - routeModuleIndex?: number + url: URL ): void; } diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index ce477fdf6bd..c33bd5c4b6c 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -360,8 +360,8 @@ async function pureServerFunction(ev: RequestEvent) { function fixTrailingSlash(ev: RequestEvent) { const trailingSlash = getRequestTrailingSlash(ev); - const { basePathname, canonicalUrl, sharedMap } = ev; - const { pathname, search } = canonicalUrl; + const { basePathname, originalUrl, sharedMap } = ev; + const { pathname, search } = originalUrl; const isQData = sharedMap.has(IsQData); if (!isQData && pathname !== basePathname && !pathname.endsWith('.html')) { // only check for slash redirect on pages diff --git a/packages/qwik-city/src/middleware/request-handler/response-page.ts b/packages/qwik-city/src/middleware/request-handler/response-page.ts index 0be3099a785..d374f790d89 100644 --- a/packages/qwik-city/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-city/src/middleware/request-handler/response-page.ts @@ -10,7 +10,7 @@ import { } from './request-event'; export function getQwikCityServerData(requestEv: RequestEvent) { - const { params, request, status, locale, canonicalUrl } = requestEv; + const { params, request, status, locale, originalUrl } = requestEv; const requestHeaders: Record = {}; request.headers.forEach((value, key) => (requestHeaders[key] = value)); @@ -19,7 +19,7 @@ export function getQwikCityServerData(requestEv: RequestEvent) { const routeName = requestEv.sharedMap.get(RequestRouteName) as string; const nonce = requestEv.sharedMap.get(RequestEvSharedNonce); const headers = requestEv.request.headers; - const reconstructedUrl = new URL(canonicalUrl.pathname + canonicalUrl.search, canonicalUrl); + const reconstructedUrl = new URL(originalUrl.pathname + originalUrl.search, originalUrl); const host = headers.get('X-Forwarded-Host')!; const protocol = headers.get('X-Forwarded-Proto')!; if (host) { diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 9fe060ff83a..13b037bf57e 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -203,16 +203,13 @@ export interface RequestEventCommon readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; /** - * URL to rewrite to. When called, the flow will reset to display the given url route. + * When called, qwik-city will execute the path's matching route flow. * - * URL will remain unchanged in the browser history. + * The url in the browser will remain unchanged. * - * Note that if the url is a string without a protocol, the rewritten url will inherit the search - * params from the canonical url. - * - * @param url - The url to rewrite to. + * @param pathname - The pathname to rewrite to. */ - readonly rewrite: (url: string | URL) => RewriteMessage; + readonly rewrite: (pathname: string) => RewriteMessage; /** * When called, the response will immediately end with the given status code. This could be useful @@ -305,13 +302,13 @@ export interface RequestEventBase { * * This property was introduced to support the rewrite feature. * - * If rewrite is called, the canonicalUrl will remain as the original url, and the url property - * will be changed to the rewritten url. + * If rewrite is called, the url property will be changed to the rewritten url. while originalUrl + * will stay the same(e.g the url inserted to the address bar). * - * If rewrite is never called as part of the request, the url property and the canonicalUrl are - * considered equal. + * If rewrite is never called as part of the request, the url property and the originalUrl are + * equal. */ - readonly canonicalUrl: URL; + readonly originalUrl: URL; /** The base pathname of the request, which can be configured at build time. Defaults to `/`. */ readonly basePathname: string; diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 8426301bace..b267e425310 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -130,7 +130,7 @@ async function runNext( rewriteAttempt += 1; const url = new URL(requestEv.headers.get('Rewrite-Location')!); const { loadedRoute, requestHandlers } = await rebuildRouteInfo(url); - requestEv.replay(loadedRoute, requestHandlers, url); + requestEv.resetRoute(loadedRoute, requestHandlers, url); runResult = await _runNext(); } diff --git a/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx b/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx index 7384e92fdf1..b7b4bcb8931 100644 --- a/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx +++ b/starters/apps/qwikcity-test/src/routes/(common)/products/[id]/index.tsx @@ -70,24 +70,33 @@ export default component$(() => {
  • T-Shirt (Rewrite to /products/tshirt)
  • T-Shirt (Rewrite to /products/tshirt)
  • + T-Shirt (Rewrite to /products/tshirt) Also trailing slash should be + added. + +
  • +
  • + T-Shirt (Rewrite to /products/tshirt) Also trailing slash should be added. @@ -163,13 +172,13 @@ export const useProductLoader = routeLoader$( throw redirect(301, "/qwikcity-test/products/tshirt/"); } - if (id === "shirt-rewrite-plain_string") { + if (id === "shirt-rewrite") { throw rewrite("/qwikcity-test/products/tshirt/"); } - if (id === "shirt-rewrite-url_object") { - // Rewrite with a URL object. - throw rewrite(new URL("/qwikcity-test/products/tshirt/", url.origin)); + // Should throw an error + if (id === "shirt-rewrite-absolute-url") { + throw rewrite(`${url.origin}/qwikcity-test/products/tshirt/`); } if (id === "error") { diff --git a/starters/e2e/qwikcity/page.spec.ts b/starters/e2e/qwikcity/page.spec.ts index 8cfde6f30e4..55c01ad1758 100644 --- a/starters/e2e/qwikcity/page.spec.ts +++ b/starters/e2e/qwikcity/page.spec.ts @@ -1,5 +1,12 @@ import { expect, test } from "@playwright/test"; -import { assertPage, linkNavigate, load, locator } from "./util.js"; +import { + assertPage, + getPage, + linkNavigate, + load, + locator, + setPage, +} from "./util.js"; test.describe("Qwik City Page", () => { test.describe("mpa", () => { @@ -110,12 +117,9 @@ function tests() { }); /*********** Products: shirt (rewrite to /products/tshirt) ***********/ - await linkNavigate( - ctx, - '[data-test-link="products-shirt-rewrite-plain_string"]', - ); + await linkNavigate(ctx, '[data-test-link="products-shirt-rewrite"]'); await assertPage(ctx, { - pathname: "/qwikcity-test/products/shirt-rewrite-plain_string/", + pathname: "/qwikcity-test/products/shirt-rewrite/", title: "Product tshirt - Qwik", layoutHierarchy: ["root"], h1: "Product: tshirt", @@ -125,23 +129,36 @@ function tests() { /*********** Products: shirt (rewrite to /products/tshirt) ***********/ await linkNavigate( ctx, - '[data-test-link="products-shirt-rewrite-url_object"]', + '[data-test-link="products-shirt-rewrite-with-search"]', ); await assertPage(ctx, { - pathname: "/qwikcity-test/products/shirt-rewrite-url_object/", + pathname: "/qwikcity-test/products/shirt-rewrite/", title: "Product tshirt - Qwik", layoutHierarchy: ["root"], h1: "Product: tshirt", activeHeaderLink: "Products", + searchParams: { search: "true" }, + }); + + /*********** Products: shirt (rewrite to /products/tshirt) ***********/ + await linkNavigate( + ctx, + '[data-test-link="products-shirt-rewrite-absolute-url"]', + 500, + ); + await assertPage(ctx, { + title: "500 Internal Server Error", }); + // Recover from error + await setPage(ctx, "/qwikcity-test/products/hat/"); /*********** Products: shirt (rewrite to /products/tshirt) ***********/ await linkNavigate( ctx, - '[data-test-link="products-shirt-rewrite-plain_string_no_trailing_slash"]', + '[data-test-link="products-shirt-rewrite-no-trailing-slash"]', ); await assertPage(ctx, { - pathname: "/qwikcity-test/products/shirt-rewrite-plain_string/", + pathname: "/qwikcity-test/products/shirt-rewrite/", title: "Product tshirt - Qwik", layoutHierarchy: ["root"], h1: "Product: tshirt", diff --git a/starters/e2e/qwikcity/util.ts b/starters/e2e/qwikcity/util.ts index c59748a8011..8f763ede2b5 100644 --- a/starters/e2e/qwikcity/util.ts +++ b/starters/e2e/qwikcity/util.ts @@ -177,6 +177,16 @@ export function getPage(ctx: TestContext) { return ctx.browserContext.pages()[0]!; } +export async function setPage(ctx: TestContext, pathname: string) { + const page = getPage(ctx); + const response = (await page.goto(pathname))!; + const status = response.status(); + if (status !== 200) { + const text = await response.text(); + expect(status, `${pathname} (${status})\n${text}`).toBe(200); + } +} + export async function load( browserContext: BrowserContext, javaScriptEnabled: boolean | undefined, From 581f2676d84bf9d7c23fee01b588fbf90d21219c Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 6 May 2025 07:58:13 +0300 Subject: [PATCH 31/37] minor docs change --- .../src/routes/docs/(qwikcity)/guides/rewrites/index.mdx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx index 2d9e2218f51..70ca044e1fb 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/guides/rewrites/index.mdx @@ -38,11 +38,8 @@ export const onRequest: RequestHandler = async ({ url, rewrite }) => { }; ``` -The `rewrite()` function, which was destructured in the RequestHandler function arguments, takes a pathname string or a URL object. +The `rewrite()` function, which was destructured in the RequestHandler function arguments, is invoked with a pathname string. ```tsx throw rewrite(`/articles/777/`); -throw rewrite(new URL(`/articles/777/`, url)); ``` - -Note: If you provide a relative path, Qwik City will rewrite the URL and apply search params from the original URL. From c345f11a8b6d805b4e01eb2da355903791dd5a84 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 6 May 2025 08:10:26 +0300 Subject: [PATCH 32/37] minor docs change --- .../routes/api/qwik-city-middleware-request-handler/api.json | 2 +- .../routes/api/qwik-city-middleware-request-handler/index.mdx | 2 +- packages/qwik-city/src/middleware/request-handler/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index 4ffe5d338e0..dd5a662c957 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -313,7 +313,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventBase \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[basePathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nThe base pathname of the request, which can be configured at build time. Defaults to `/`.\n\n\n
    \n\n[cacheControl](#)\n\n\n\n\n`readonly`\n\n\n\n\n(cacheControl: [CacheControl](#cachecontrol), target?: CacheControlTarget) => void\n\n\n\n\nConvenience method to set the Cache-Control header. Depending on your CDN, you may want to add another cacheControl with the second argument set to `CDN-Cache-Control` or any other value (we provide the most common values for auto-complete, but you can use any string you want).\n\nSee https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and https://qwik.dev/docs/caching/\\#CDN-Cache-Controls for more information.\n\n\n
    \n\n[clientConn](#)\n\n\n\n\n`readonly`\n\n\n\n\n[ClientConn](#clientconn)\n\n\n\n\nProvides information about the client connection, such as the IP address and the country the request originated from.\n\n\n
    \n\n[cookie](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Cookie](#cookie)\n\n\n\n\nHTTP request and response cookie. Use the `get()` method to retrieve a request cookie value. Use the `set()` method to set a response cookie value.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n\n\n
    \n\n[env](#)\n\n\n\n\n`readonly`\n\n\n\n\n[EnvGetter](#envgetter)\n\n\n\n\nPlatform provided environment variables.\n\n\n
    \n\n[headers](#)\n\n\n\n\n`readonly`\n\n\n\n\nHeaders\n\n\n\n\nHTTP response headers. Notice it will be empty until you first add a header. If you want to read the request headers, use `request.headers` instead.\n\nhttps://developer.mozilla.org/en-US/docs/Glossary/Response\\_header\n\n\n
    \n\n[method](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nHTTP request method.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\n\n
    \n\n[originalUrl](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request Canonical URL.\n\nThis property was introduced to support the rewrite feature.\n\nIf rewrite is called, the url property will be changed to the rewritten url. while originalUrl will stay the same(e.g the url inserted to the address bar).\n\nIf rewrite is never called as part of the request, the url property and the originalUrl are equal.\n\n\n
    \n\n[params](#)\n\n\n\n\n`readonly`\n\n\n\n\nReadonly<Record<string, string>>\n\n\n\n\nURL path params which have been parsed from the current url pathname segments. Use `query` to instead retrieve the query string search params.\n\n\n
    \n\n[parseBody](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<unknown>\n\n\n\n\nThis method will check the request headers for a `Content-Type` header and parse the body accordingly. It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.\n\nIf the `Content-Type` header is not set, it will return `null`.\n\n\n
    \n\n[pathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nURL pathname. Does not include the protocol, domain, query string (search params) or hash.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URL/pathname\n\n\n
    \n\n[platform](#)\n\n\n\n\n`readonly`\n\n\n\n\nPLATFORM\n\n\n\n\nPlatform specific data and functions\n\n\n
    \n\n[query](#)\n\n\n\n\n`readonly`\n\n\n\n\nURLSearchParams\n\n\n\n\nURL Query Strings (URL Search Params). Use `params` to instead retrieve the route params found in the url pathname.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n\n\n
    \n\n[request](#)\n\n\n\n\n`readonly`\n\n\n\n\nRequest\n\n\n\n\nHTTP request information.\n\n\n
    \n\n[sharedMap](#)\n\n\n\n\n`readonly`\n\n\n\n\nMap<string, any>\n\n\n\n\nShared Map across all the request handlers. Every HTTP request will get a new instance of the shared map. The shared map is useful for sharing data between request handlers.\n\n\n
    \n\n[signal](#)\n\n\n\n\n`readonly`\n\n\n\n\nAbortSignal\n\n\n\n\nRequest's AbortSignal (same as `request.signal`). This signal indicates that the request has been aborted.\n\n\n
    \n\n[url](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request URL.\n\n\n
    ", + "content": "```typescript\nexport interface RequestEventBase \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[basePathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nThe base pathname of the request, which can be configured at build time. Defaults to `/`.\n\n\n
    \n\n[cacheControl](#)\n\n\n\n\n`readonly`\n\n\n\n\n(cacheControl: [CacheControl](#cachecontrol), target?: CacheControlTarget) => void\n\n\n\n\nConvenience method to set the Cache-Control header. Depending on your CDN, you may want to add another cacheControl with the second argument set to `CDN-Cache-Control` or any other value (we provide the most common values for auto-complete, but you can use any string you want).\n\nSee https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control and https://qwik.dev/docs/caching/\\#CDN-Cache-Controls for more information.\n\n\n
    \n\n[clientConn](#)\n\n\n\n\n`readonly`\n\n\n\n\n[ClientConn](#clientconn)\n\n\n\n\nProvides information about the client connection, such as the IP address and the country the request originated from.\n\n\n
    \n\n[cookie](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Cookie](#cookie)\n\n\n\n\nHTTP request and response cookie. Use the `get()` method to retrieve a request cookie value. Use the `set()` method to set a response cookie value.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n\n\n
    \n\n[env](#)\n\n\n\n\n`readonly`\n\n\n\n\n[EnvGetter](#envgetter)\n\n\n\n\nPlatform provided environment variables.\n\n\n
    \n\n[headers](#)\n\n\n\n\n`readonly`\n\n\n\n\nHeaders\n\n\n\n\nHTTP response headers. Notice it will be empty until you first add a header. If you want to read the request headers, use `request.headers` instead.\n\nhttps://developer.mozilla.org/en-US/docs/Glossary/Response\\_header\n\n\n
    \n\n[method](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nHTTP request method.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\n\n
    \n\n[originalUrl](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nThe original HTTP request URL.\n\nThis property was introduced to support the rewrite feature.\n\nIf rewrite is called, the url property will be changed to the rewritten url. while originalUrl will stay the same(e.g the url inserted to the address bar).\n\nIf rewrite is never called as part of the request, the url property and the originalUrl are equal.\n\n\n
    \n\n[params](#)\n\n\n\n\n`readonly`\n\n\n\n\nReadonly<Record<string, string>>\n\n\n\n\nURL path params which have been parsed from the current url pathname segments. Use `query` to instead retrieve the query string search params.\n\n\n
    \n\n[parseBody](#)\n\n\n\n\n`readonly`\n\n\n\n\n() => Promise<unknown>\n\n\n\n\nThis method will check the request headers for a `Content-Type` header and parse the body accordingly. It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.\n\nIf the `Content-Type` header is not set, it will return `null`.\n\n\n
    \n\n[pathname](#)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\nURL pathname. Does not include the protocol, domain, query string (search params) or hash.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URL/pathname\n\n\n
    \n\n[platform](#)\n\n\n\n\n`readonly`\n\n\n\n\nPLATFORM\n\n\n\n\nPlatform specific data and functions\n\n\n
    \n\n[query](#)\n\n\n\n\n`readonly`\n\n\n\n\nURLSearchParams\n\n\n\n\nURL Query Strings (URL Search Params). Use `params` to instead retrieve the route params found in the url pathname.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n\n\n
    \n\n[request](#)\n\n\n\n\n`readonly`\n\n\n\n\nRequest\n\n\n\n\nHTTP request information.\n\n\n
    \n\n[sharedMap](#)\n\n\n\n\n`readonly`\n\n\n\n\nMap<string, any>\n\n\n\n\nShared Map across all the request handlers. Every HTTP request will get a new instance of the shared map. The shared map is useful for sharing data between request handlers.\n\n\n
    \n\n[signal](#)\n\n\n\n\n`readonly`\n\n\n\n\nAbortSignal\n\n\n\n\nRequest's AbortSignal (same as `request.signal`). This signal indicates that the request has been aborted.\n\n\n
    \n\n[url](#)\n\n\n\n\n`readonly`\n\n\n\n\nURL\n\n\n\n\nHTTP request URL.\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts", "mdFile": "qwik-city.requesteventbase.md" }, diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index a4ac0f73c46..d5c69216f05 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -1032,7 +1032,7 @@ URL -HTTP request Canonical URL. +The original HTTP request URL. This property was introduced to support the rewrite feature. diff --git a/packages/qwik-city/src/middleware/request-handler/types.ts b/packages/qwik-city/src/middleware/request-handler/types.ts index 13b037bf57e..659ce5bbe74 100644 --- a/packages/qwik-city/src/middleware/request-handler/types.ts +++ b/packages/qwik-city/src/middleware/request-handler/types.ts @@ -298,7 +298,7 @@ export interface RequestEventBase { readonly url: URL; /** - * HTTP request Canonical URL. + * The original HTTP request URL. * * This property was introduced to support the rewrite feature. * From 8e1a150bf6f740e0b68c5dbb64b4cebadc95119f Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 6 May 2025 18:53:22 +0300 Subject: [PATCH 33/37] CR fixes -2- --- .../api.json | 19 +++++- .../index.mdx | 66 +++++++++++++++++++ .../middleware.request-handler.api.md | 3 + .../request-handler/request-event.ts | 16 +---- .../resolve-request-handlers.ts | 39 +---------- .../request-handler/rewrite-handler.ts | 6 +- .../request-handler/user-response.ts | 22 +++---- .../src/runtime/src/qwik-city-component.tsx | 4 +- packages/qwik-city/src/runtime/src/types.ts | 2 +- 9 files changed, 108 insertions(+), 69 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json index dd5a662c957..4fd4315b9fe 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json @@ -261,6 +261,23 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/cookie.ts", "mdFile": "qwik-city.mergeheaderscookies.md" }, + { + "name": "pathname", + "id": "rewritemessage-pathname", + "hierarchy": [ + { + "name": "RewriteMessage", + "id": "rewritemessage-pathname" + }, + { + "name": "pathname", + "id": "rewritemessage-pathname" + } + ], + "kind": "Property", + "content": "```typescript\nreadonly pathname: string;\n```", + "mdFile": "qwik-city.rewritemessage.pathname.md" + }, { "name": "RedirectMessage", "id": "redirectmessage", @@ -411,7 +428,7 @@ } ], "kind": "Class", - "content": "```typescript\nexport declare class RewriteMessage extends AbortMessage \n```\n**Extends:** [AbortMessage](#abortmessage)", + "content": "```typescript\nexport declare class RewriteMessage extends AbortMessage \n```\n**Extends:** [AbortMessage](#abortmessage)\n\n\n\n\n
    \n\nConstructor\n\n\n\n\nModifiers\n\n\n\n\nDescription\n\n\n
    \n\n[(constructor)(pathname)](#)\n\n\n\n\n\n\n\nConstructs a new instance of the `RewriteMessage` class\n\n\n
    \n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[pathname](#rewritemessage-pathname)\n\n\n\n\n`readonly`\n\n\n\n\nstring\n\n\n\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts", "mdFile": "qwik-city.rewritemessage.md" }, diff --git a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx index d5c69216f05..a5255b09275 100644 --- a/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx @@ -717,6 +717,12 @@ Headers [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/cookie.ts) +## pathname + +```typescript +readonly pathname: string; +``` + ## RedirectMessage ```typescript @@ -1512,6 +1518,66 @@ export declare class RewriteMessage extends AbortMessage **Extends:** [AbortMessage](#abortmessage) + + +
    + +Constructor + + + +Modifiers + + + +Description + +
    + +[(constructor)(pathname)](#) + + + + + +Constructs a new instance of the `RewriteMessage` class + +
    + + + +
    + +Property + + + +Modifiers + + + +Type + + + +Description + +
    + +[pathname](#rewritemessage-pathname) + + + +`readonly` + + + +string + + + +
    + [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts) ## ServerError diff --git a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md index f475a4ab0c9..aac2956b51c 100644 --- a/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md @@ -178,6 +178,9 @@ export interface ResolveValue { // @public (undocumented) export class RewriteMessage extends AbortMessage { + constructor(pathname: string); + // (undocumented) + readonly pathname: string; } // @public (undocumented) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 22ae1cb65dd..4988c731401 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -37,6 +37,7 @@ export const RequestRouteName = '@routeName'; export const RequestEvSharedActionId = '@actionId'; export const RequestEvSharedActionFormData = '@actionFormData'; export const RequestEvSharedNonce = '@nonce'; +export const RequestEvIsRewrite = '@rewrite'; export function createRequestEvent( serverRequestEv: ServerRequestEvent, @@ -249,19 +250,8 @@ export function createRequestEvent( if (pathname.startsWith('http')) { throw new Error('Rewrite does not support absolute urls'); } - - const rewriteUrl = new URL(url); - rewriteUrl.pathname = pathname; - - // Fix consecutive slashes - e.g //path//to//page -> /path/to/page - const fixedPathname = rewriteUrl.pathname.replace(/(^|[^:])\/{2,}/g, '$1/'); - if (rewriteUrl.pathname !== fixedPathname) { - console.warn(`Rewrite URL ${rewriteUrl.pathname} is invalid, fixing to ${fixedPathname}`); - rewriteUrl.pathname = fixedPathname; - } - - headers.set('Rewrite-Location', rewriteUrl.toString()); - return new RewriteMessage(); + sharedMap.set(RequestEvIsRewrite, true); + return new RewriteMessage(pathname.replace(/\/+/g, '/')); }, defer: (returnData) => { diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index c33bd5c4b6c..7ef8f79cbc1 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -14,9 +14,9 @@ import type { } from '../../runtime/src/types'; import { HttpStatus } from './http-status-codes'; import { RedirectMessage } from './redirect-handler'; -import { RewriteMessage } from './rewrite-handler'; import { RequestEvQwikSerializer, + RequestEvIsRewrite, RequestEvSharedActionId, RequestRouteName, getRequestLoaders, @@ -79,7 +79,6 @@ export const resolveRequestHandlers = ( } const routeModules = route[2]; requestHandlers.push(handleRedirect); - requestHandlers.push(handleRewrite); _resolveRequestHandlers( routeLoaders, routeActions, @@ -536,39 +535,6 @@ export async function handleRedirect(requestEv: RequestEvent) { } } -export async function handleRewrite(requestEv: RequestEvent) { - const isPageDataReq = requestEv.sharedMap.has(IsQData); - if (!isPageDataReq) { - return; - } - try { - await requestEv.next(); - } catch (err) { - if (!(err instanceof RewriteMessage)) { - throw err; - } - } - - if (requestEv.headersSent) { - return; - } - - const status = requestEv.status(); - const location = requestEv.headers.get('Rewrite-Location'); - const isRewrite = status === 200 && location; - - if (isRewrite) { - const adaptedLocation = makeQDataPath(location); - - if (adaptedLocation) { - requestEv.headers.set('Rewrite-Location', adaptedLocation); - return; - } else { - requestEv.status(200); - } - } -} - export async function renderQData(requestEv: RequestEvent) { const isPageDataReq = requestEv.sharedMap.has(IsQData); if (!isPageDataReq) { @@ -582,7 +548,6 @@ export async function renderQData(requestEv: RequestEvent) { const status = requestEv.status(); const redirectLocation = requestEv.headers.get('Location'); - const rewriteLocation = requestEv.headers.get('Rewrite-Location'); const trailingSlash = getRequestTrailingSlash(requestEv); const requestHeaders: Record = {}; @@ -595,7 +560,7 @@ export async function renderQData(requestEv: RequestEvent) { status: status !== 200 ? status : 200, href: getPathname(requestEv.url, trailingSlash), redirect: redirectLocation ?? undefined, - rewrite: rewriteLocation ?? undefined, + isRewrite: requestEv.sharedMap.get(RequestEvIsRewrite), }; const writer = requestEv.getWritableStream().getWriter(); const qwikSerializer = (requestEv as RequestEventInternal)[RequestEvQwikSerializer]; diff --git a/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts b/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts index eb65b547a12..143d1db3172 100644 --- a/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts @@ -1,4 +1,8 @@ import { AbortMessage } from './redirect-handler'; /** @public */ -export class RewriteMessage extends AbortMessage {} +export class RewriteMessage extends AbortMessage { + constructor(readonly pathname: string) { + super(); + } +} diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index b267e425310..9750fd669bd 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -68,11 +68,8 @@ async function runNext( resolve: (value: any) => void ) { let rewriteAttempt = 1; - let didRewrite = false; async function _runNext() { - didRewrite = false; - try { // Run all middlewares await requestEv.next(); @@ -85,7 +82,12 @@ async function runNext( throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); } - didRewrite = true; + rewriteAttempt += 1; + const url = new URL(requestEv.url); + url.pathname = e.pathname; + const { loadedRoute, requestHandlers } = await rebuildRouteInfo(url); + requestEv.resetRoute(loadedRoute, requestHandlers, url); + return await _runNext(); } else if (e instanceof ServerError) { if (!requestEv.headersSent) { const status = e.status as StatusCodes; @@ -117,6 +119,7 @@ async function runNext( console.error('Unable to render error page'); } } + return e; } } @@ -125,16 +128,7 @@ async function runNext( } try { - let runResult = await _runNext(); - if (didRewrite) { - rewriteAttempt += 1; - const url = new URL(requestEv.headers.get('Rewrite-Location')!); - const { loadedRoute, requestHandlers } = await rebuildRouteInfo(url); - requestEv.resetRoute(loadedRoute, requestHandlers, url); - runResult = await _runNext(); - } - - return runResult; + return await _runNext(); } finally { if (!requestEv.isDirty()) { resolve(null); diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index 14a695fe132..7baa11ff303 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -346,7 +346,7 @@ export const QwikCityProvider = component$((props) => { const newURL = new URL(newHref, trackUrl); if (!isSamePath(newURL, trackUrl)) { // Change our path to the canonical path in the response unless rewrite. - if (!pageData.rewrite) { + if (!pageData.isRewrite) { trackUrl = newURL; } @@ -354,7 +354,7 @@ export const QwikCityProvider = component$((props) => { qwikCity.routes, qwikCity.menus, qwikCity.cacheModules, - newURL.pathname // Load the canonical path. + newURL.pathname // Load the actual required path. ); } diff --git a/packages/qwik-city/src/runtime/src/types.ts b/packages/qwik-city/src/runtime/src/types.ts index 767a81db896..d5f0af33201 100644 --- a/packages/qwik-city/src/runtime/src/types.ts +++ b/packages/qwik-city/src/runtime/src/types.ts @@ -311,7 +311,7 @@ export interface ClientPageData extends Omit { status: number; href: string; redirect?: string; - rewrite?: string; + isRewrite?: boolean; } /** @public */ From 97b3cb7c95e031fc4edb72b1d8c71c7c17ef6aba Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Mon, 12 May 2025 14:49:15 +0300 Subject: [PATCH 34/37] CR fixes --- .../qwik-city/src/middleware/request-handler/user-response.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 9750fd669bd..6802baf7fbe 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -78,8 +78,8 @@ async function runNext( const stream = requestEv.getWritableStream(); await stream.close(); } else if (e instanceof RewriteMessage) { - if (rewriteAttempt > 5) { - throw new Error(`Rewrite failed - Max rewrite attempts reached: ${rewriteAttempt - 1}`); + if (rewriteAttempt > 50) { + throw new Error(`Infinite rewrite loop`); } rewriteAttempt += 1; From 67a1b931521b655cced70ee70aa5671aee0e4995 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 20 May 2025 18:01:10 +0300 Subject: [PATCH 35/37] make it experimental --- .../src/routes/api/qwik-optimizer/api.json | 19 ++++++++++++++++++- .../src/routes/api/qwik-optimizer/index.mdx | 15 +++++++++++++++ .../request-handler/request-event.ts | 5 +++++ .../qwik/src/optimizer/src/plugins/plugin.ts | 2 ++ .../src/optimizer/src/qwik.optimizer.api.md | 1 + 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index 9623faed9d8..1e162f4e651 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -106,6 +106,23 @@ "content": "```typescript\ndirname(path: string): string;\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\npath\n\n\n\n\nstring\n\n\n\n\n\n
    \n**Returns:**\n\nstring", "mdFile": "qwik.path.dirname.md" }, + { + "name": "enableRequestRewrite", + "id": "experimentalfeatures-enablerequestrewrite", + "hierarchy": [ + { + "name": "ExperimentalFeatures", + "id": "experimentalfeatures-enablerequestrewrite" + }, + { + "name": "enableRequestRewrite", + "id": "experimentalfeatures-enablerequestrewrite" + } + ], + "kind": "EnumMember", + "content": "", + "mdFile": "qwik.experimentalfeatures.enablerequestrewrite.md" + }, { "name": "EntryStrategy", "id": "entrystrategy", @@ -130,7 +147,7 @@ } ], "kind": "Enum", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n\n\n\n\n
    \n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
    \n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
    \n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
    \n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
    ", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n\n\n\n\n\n
    \n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
    \n\nenableRequestRewrite\n\n\n\n\n`\"enableRequestRewrite\"`\n\n\n\n\n**_(ALPHA)_** Enable request.rewrite()\n\n\n
    \n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
    \n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
    \n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts", "mdFile": "qwik.experimentalfeatures.md" }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.mdx b/packages/docs/src/routes/api/qwik-optimizer/index.mdx index bc784f63ff0..5a90383bf3d 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.mdx +++ b/packages/docs/src/routes/api/qwik-optimizer/index.mdx @@ -325,6 +325,8 @@ string string +## enableRequestRewrite + ## EntryStrategy ```typescript @@ -369,6 +371,19 @@ Description +enableRequestRewrite + + + +`"enableRequestRewrite"` + + + +**_(ALPHA)_** Enable request.rewrite() + + + + noSPA diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 4988c731401..a3e2a0add64 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -246,6 +246,11 @@ export function createRequestEvent( }, rewrite: (pathname: string) => { + if (!__EXPERIMENTAL__.enableRequestRewrite) { + throw new Error( + 'enableRequestRewrite is experimental and must be enabled with `experimental: ["enableRequestRewrite"]` in the `qwikVite` plugin.' + ); + } check(); if (pathname.startsWith('http')) { throw new Error('Rewrite does not support absolute urls'); diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index ccd167b3dfa..1d29cbc6344 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -73,6 +73,8 @@ export enum ExperimentalFeatures { valibot = 'valibot', /** Disable SPA navigation handler in Qwik City */ noSPA = 'noSPA', + /** Enable request.rewrite() */ + enableRequestRewrite = 'enableRequestRewrite', } export interface QwikPackages { diff --git a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md index 189019b96bc..e34d109f178 100644 --- a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md +++ b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md @@ -52,6 +52,7 @@ export type EntryStrategy = InlineEntryStrategy | HoistEntryStrategy | SingleEnt // @alpha export enum ExperimentalFeatures { + enableRequestRewrite = "enableRequestRewrite", noSPA = "noSPA", preventNavigate = "preventNavigate", valibot = "valibot" From 6e021fa13dd569579504e7431431d1f62cd3c33c Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 20 May 2025 19:32:26 +0300 Subject: [PATCH 36/37] Fix the FF issues. --- .../src/middleware/request-handler/request-event.ts | 5 ----- .../qwik-city/src/runtime/src/qwik-city-component.tsx | 9 +++++++++ starters/dev-server.ts | 6 +++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index a3e2a0add64..4988c731401 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -246,11 +246,6 @@ export function createRequestEvent( }, rewrite: (pathname: string) => { - if (!__EXPERIMENTAL__.enableRequestRewrite) { - throw new Error( - 'enableRequestRewrite is experimental and must be enabled with `experimental: ["enableRequestRewrite"]` in the `qwikVite` plugin.' - ); - } check(); if (pathname.startsWith('http')) { throw new Error('Rewrite does not support absolute urls'); diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index 7baa11ff303..775ae27bc67 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -116,6 +116,15 @@ export const QwikCityProvider = component$((props) => { throw new Error(`Missing Qwik URL Env Data`); } + if (isServer) { + if ( + env!.ev.originalUrl.pathname !== env!.ev.url.pathname && + !__EXPERIMENTAL__.enableRequestRewrite + ) { + throw new Error(`enableRequestRewrite: ${__EXPERIMENTAL__.enableRequestRewrite}`); + } + } + const url = new URL(urlEnv); const routeLocation = useStore( { diff --git a/starters/dev-server.ts b/starters/dev-server.ts index f1021cbc46f..5235459973e 100644 --- a/starters/dev-server.ts +++ b/starters/dev-server.ts @@ -210,7 +210,7 @@ export { optimizer.qwikVite({ /** * normally qwik finds qwik-city via package.json but we don't want that - * because it causes it try try to lookup the special qwik city imports + * because it causes it to try to lookup the special qwik city imports * even when we're not actually importing qwik-city */ disableVendorScan: true, @@ -223,7 +223,7 @@ export { clientManifest = manifest; }, }, - experimental: ["preventNavigate"], + experimental: ["preventNavigate", "enableRequestRewrite"], }), ], }), @@ -240,7 +240,7 @@ export { plugins: [ ...plugins, optimizer.qwikVite({ - experimental: ["preventNavigate"], + experimental: ["preventNavigate", "enableRequestRewrite"], }), ], define: { From fc8ac2159c0d6f4e9f8d001ad2e3df50a1e2ad37 Mon Sep 17 00:00:00 2001 From: Omer Prizner Date: Tue, 20 May 2025 19:40:18 +0300 Subject: [PATCH 37/37] re-add bad error description --- packages/qwik-city/src/runtime/src/qwik-city-component.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx index 775ae27bc67..6bb2099a849 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx @@ -121,7 +121,9 @@ export const QwikCityProvider = component$((props) => { env!.ev.originalUrl.pathname !== env!.ev.url.pathname && !__EXPERIMENTAL__.enableRequestRewrite ) { - throw new Error(`enableRequestRewrite: ${__EXPERIMENTAL__.enableRequestRewrite}`); + throw new Error( + `enableRequestRewrite is an experimental feature and is not enabled. Please enable the feature flag by adding \`experimental: ["enableRequestRewrite"]\` to your qwikVite plugin options.` + ); } }