Skip to content

Commit 7b7e0d7

Browse files
authored
Support rewrite request (Similarly to redirect) (#7562)
* WIP * handleRewrite WIP * distinguish between redirect and rewrite * header property Location is a specific header meant for redirects, removing it breaks redirects * rewrite finally doesnt throw error > behaves as redirect if the status isnt a 301+. still WIP, need to remove consoles. * WIP - rewrite somewhat works * Rewrite works. * remove console logs * fix unreadable condition + remove leftover log * bring back accidental deleted log * tidy up before tests * support rewrite for URL objects * minor rename * properly handle urls+pathnames+searchparams * more readable loadedRoute handling. fixed params assignement issue * fix some types. WIP e2e tests - for some reason not working due to pathname missmatch * attempt to fix the rewrite in preview - no luck * texts * fix preview whitescreen. * introduce canonicalUrl - to know the request's original url * everything works including e2e's. will write some more and we're good to go * everything works well. * bump * docs * remove logs * minor simplification * more self pr * fixed it all, finally. * api updated * CR fixes * minor docs change * minor docs change * CR fixes -2- * CR fixes * make it experimental * Fix the FF issues. * re-add bad error description
1 parent 9cd62bd commit 7b7e0d7

File tree

24 files changed

+625
-65
lines changed

24 files changed

+625
-65
lines changed

.changeset/dirty-dolls-heal.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@builder.io/qwik-city': minor
3+
---
4+
5+
FEAT: Added rewrite() to the RequestEvent object. It works like redirect but does not change the URL,
6+
think of it as an internal redirect.
7+
8+
Example usage:
9+
```ts
10+
export const onRequest: RequestHandler = async ({ url, rewrite }) => {
11+
if (url.pathname.includes("/articles/the-best-article-in-the-world")) {
12+
const artistId = db.getArticleByName("the-best-article-in-the-world");
13+
14+
// Url will remain /articles/the-best-article-in-the-world, but under the hood,
15+
// will render /articles/${artistId}
16+
throw rewrite(`/articles/${artistId}`);
17+
}
18+
};
19+
```

packages/docs/src/routes/api/qwik-city-middleware-request-handler/api.json

Lines changed: 33 additions & 2 deletions
Large diffs are not rendered by default.

packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.mdx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,12 @@ Headers
717717

718718
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/cookie.ts)
719719

720+
## pathname
721+
722+
```typescript
723+
readonly pathname: string;
724+
```
725+
720726
## RedirectMessage
721727

722728
```typescript
@@ -1020,6 +1026,29 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
10201026
</td></tr>
10211027
<tr><td>
10221028
1029+
[originalUrl](#)
1030+
1031+
</td><td>
1032+
1033+
`readonly`
1034+
1035+
</td><td>
1036+
1037+
URL
1038+
1039+
</td><td>
1040+
1041+
The original HTTP request URL.
1042+
1043+
This property was introduced to support the rewrite feature.
1044+
1045+
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).
1046+
1047+
If rewrite is never called as part of the request, the url property and the originalUrl are equal.
1048+
1049+
</td></tr>
1050+
<tr><td>
1051+
10231052
[params](#)
10241053
10251054
</td><td>
@@ -1312,6 +1341,25 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
13121341
</td></tr>
13131342
<tr><td>
13141343
1344+
[rewrite](#)
1345+
1346+
</td><td>
1347+
1348+
`readonly`
1349+
1350+
</td><td>
1351+
1352+
(pathname: string) =&gt; [RewriteMessage](#rewritemessage)
1353+
1354+
</td><td>
1355+
1356+
When called, qwik-city will execute the path's matching route flow.
1357+
1358+
The url in the browser will remain unchanged.
1359+
1360+
</td></tr>
1361+
<tr><td>
1362+
13151363
[send](#)
13161364
13171365
</td><td>
@@ -1462,6 +1510,76 @@ export interface ResolveValue
14621510
14631511
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts)
14641512
1513+
## RewriteMessage
1514+
1515+
```typescript
1516+
export declare class RewriteMessage extends AbortMessage
1517+
```
1518+
1519+
**Extends:** [AbortMessage](#abortmessage)
1520+
1521+
<table><thead><tr><th>
1522+
1523+
Constructor
1524+
1525+
</th><th>
1526+
1527+
Modifiers
1528+
1529+
</th><th>
1530+
1531+
Description
1532+
1533+
</th></tr></thead>
1534+
<tbody><tr><td>
1535+
1536+
[(constructor)(pathname)](#)
1537+
1538+
</td><td>
1539+
1540+
</td><td>
1541+
1542+
Constructs a new instance of the `RewriteMessage` class
1543+
1544+
</td></tr>
1545+
</tbody></table>
1546+
1547+
<table><thead><tr><th>
1548+
1549+
Property
1550+
1551+
</th><th>
1552+
1553+
Modifiers
1554+
1555+
</th><th>
1556+
1557+
Type
1558+
1559+
</th><th>
1560+
1561+
Description
1562+
1563+
</th></tr></thead>
1564+
<tbody><tr><td>
1565+
1566+
[pathname](#rewritemessage-pathname)
1567+
1568+
</td><td>
1569+
1570+
`readonly`
1571+
1572+
</td><td>
1573+
1574+
string
1575+
1576+
</td><td>
1577+
1578+
</td></tr>
1579+
</tbody></table>
1580+
1581+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/rewrite-handler.ts)
1582+
14651583
## ServerError
14661584
14671585
```typescript

packages/docs/src/routes/api/qwik-optimizer/api.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@
106106
"content": "```typescript\ndirname(path: string): string;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\npath\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nstring",
107107
"mdFile": "qwik.path.dirname.md"
108108
},
109+
{
110+
"name": "enableRequestRewrite",
111+
"id": "experimentalfeatures-enablerequestrewrite",
112+
"hierarchy": [
113+
{
114+
"name": "ExperimentalFeatures",
115+
"id": "experimentalfeatures-enablerequestrewrite"
116+
},
117+
{
118+
"name": "enableRequestRewrite",
119+
"id": "experimentalfeatures-enablerequestrewrite"
120+
}
121+
],
122+
"kind": "EnumMember",
123+
"content": "",
124+
"mdFile": "qwik.experimentalfeatures.enablerequestrewrite.md"
125+
},
109126
{
110127
"name": "EntryStrategy",
111128
"id": "entrystrategy",
@@ -130,7 +147,7 @@
130147
}
131148
],
132149
"kind": "Enum",
133-
"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<table><thead><tr><th>\n\nMember\n\n\n</th><th>\n\nValue\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nnoSPA\n\n\n</td><td>\n\n`\"noSPA\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n</td></tr>\n<tr><td>\n\npreventNavigate\n\n\n</td><td>\n\n`\"preventNavigate\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n</td></tr>\n<tr><td>\n\nvalibot\n\n\n</td><td>\n\n`\"valibot\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n</td></tr>\n</tbody></table>",
150+
"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<table><thead><tr><th>\n\nMember\n\n\n</th><th>\n\nValue\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nenableRequestRewrite\n\n\n</td><td>\n\n`\"enableRequestRewrite\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable request.rewrite()\n\n\n</td></tr>\n<tr><td>\n\nnoSPA\n\n\n</td><td>\n\n`\"noSPA\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n</td></tr>\n<tr><td>\n\npreventNavigate\n\n\n</td><td>\n\n`\"preventNavigate\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n</td></tr>\n<tr><td>\n\nvalibot\n\n\n</td><td>\n\n`\"valibot\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n</td></tr>\n</tbody></table>",
134151
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts",
135152
"mdFile": "qwik.experimentalfeatures.md"
136153
},

packages/docs/src/routes/api/qwik-optimizer/index.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ string
325325

326326
string
327327

328+
## enableRequestRewrite
329+
328330
## EntryStrategy
329331

330332
```typescript
@@ -369,6 +371,19 @@ Description
369371
</th></tr></thead>
370372
<tbody><tr><td>
371373

374+
enableRequestRewrite
375+
376+
</td><td>
377+
378+
`"enableRequestRewrite"`
379+
380+
</td><td>
381+
382+
**_(ALPHA)_** Enable request.rewrite()
383+
384+
</td></tr>
385+
<tr><td>
386+
372387
noSPA
373388

374389
</td><td>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
title: Rewrites | Guides
3+
description: Learn how to use rewrites in Qwik City.
4+
contributors:
5+
- omerman
6+
updated_at: '2025-05-04T19:43:33Z'
7+
created_at: '2025-05-04T23:45:13Z'
8+
---
9+
10+
# Rewrites
11+
12+
Sometimes you want to redirect a user from the current page to another page,
13+
but you want to keep the current URL in the browser history.
14+
15+
Let's say a user tries to access an article which is indexed by its name,
16+
e.g `/articles/qwik-is-very-quick`.
17+
but in our code, we access it by its id, on our directory structure.
18+
19+
```
20+
src/routes/articles/
21+
├── [id]
22+
├─── index.tsx
23+
```
24+
25+
26+
```tsx title="src/routes/[email protected]"
27+
import type { RequestHandler } from "@builder.io/qwik-city";
28+
29+
export const onRequest: RequestHandler = async ({ url, rewrite }) => {
30+
const pattern = /^\/articles\/(.*)$/;
31+
// Detects /articles/<article-name>, returns null if url does not match the pattern.
32+
const match = url.pathname.match(pattern);
33+
if (match) {
34+
const articleName = match[1];
35+
const { id } = await db.getArticleByName(articleName);
36+
throw rewrite(`/articles/${id}`);
37+
}
38+
};
39+
```
40+
41+
The `rewrite()` function, which was destructured in the RequestHandler function arguments, is invoked with a pathname string.
42+
43+
```tsx
44+
throw rewrite(`/articles/777/`);
45+
```

packages/qwik-city/src/buildtime/vite/dev-server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { matchRoute } from '../../runtime/src/route-matcher';
1818
import { getMenuLoader } from '../../runtime/src/routing';
1919
import type {
2020
ActionInternal,
21+
RebuildRouteInfoInternal,
2122
ContentMenu,
2223
LoadedRoute,
2324
LoaderInternal,
@@ -227,10 +228,27 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) {
227228
await server.ssrLoadModule('@qwik-serializer');
228229
const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable };
229230

231+
const rebuildRouteInfo: RebuildRouteInfoInternal = async (url: URL) => {
232+
const { serverPlugins, loadedRoute } = await resolveRoute(routeModulePaths, url);
233+
const requestHandlers = resolveRequestHandlers(
234+
serverPlugins,
235+
loadedRoute,
236+
req.method ?? 'GET',
237+
false,
238+
renderFn
239+
);
240+
241+
return {
242+
loadedRoute,
243+
requestHandlers,
244+
};
245+
};
246+
230247
const { completion, requestEv } = runQwikCity(
231248
serverRequestEv,
232249
loadedRoute,
233250
requestHandlers,
251+
rebuildRouteInfo,
234252
ctx.opts.trailingSlash,
235253
ctx.opts.basePathname,
236254
qwikSerializer

packages/qwik-city/src/middleware/request-handler/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { getErrorHtml, ServerError } from './error-handler';
22
export { mergeHeadersCookies } from './cookie';
33
export { AbortMessage, RedirectMessage } from './redirect-handler';
4+
export { RewriteMessage } from './rewrite-handler';
45
export { requestHandler } from './request-handler';
56
export { _TextEncoderStream_polyfill } from './polyfill';
67
export type {

packages/qwik-city/src/middleware/request-handler/middleware.request-handler.api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export interface RequestEventBase<PLATFORM = QwikCityPlatform> {
112112
readonly env: EnvGetter;
113113
readonly headers: Headers;
114114
readonly method: string;
115+
readonly originalUrl: URL;
115116
readonly params: Readonly<Record<string, string>>;
116117
readonly parseBody: () => Promise<unknown>;
117118
readonly pathname: string;
@@ -134,6 +135,7 @@ export interface RequestEventCommon<PLATFORM = QwikCityPlatform> extends Request
134135
readonly locale: (local?: string) => string;
135136
// Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts
136137
readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage;
138+
readonly rewrite: (pathname: string) => RewriteMessage;
137139
// Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts
138140
readonly send: SendMethod;
139141
// Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts
@@ -174,6 +176,13 @@ export interface ResolveValue {
174176
<T>(action: Action<T>): Promise<T | undefined>;
175177
}
176178

179+
// @public (undocumented)
180+
export class RewriteMessage extends AbortMessage {
181+
constructor(pathname: string);
182+
// (undocumented)
183+
readonly pathname: string;
184+
}
185+
177186
// @public (undocumented)
178187
export class ServerError<T = any> extends Error {
179188
constructor(status: number, data: T);

0 commit comments

Comments
 (0)