Skip to content

Support rewrite request (Similarly to redirect) #7562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
36d5b6b
WIP
omerman May 2, 2025
c8aab7e
handleRewrite WIP
omerman May 2, 2025
00ee036
distinguish between redirect and rewrite
omerman May 2, 2025
47157ec
header property Location is a specific header meant for redirects, re…
omerman May 2, 2025
55bb376
rewrite finally doesnt throw error > behaves as redirect if the statu…
omerman May 2, 2025
ca42587
WIP - rewrite somewhat works
omerman May 3, 2025
9c25f25
Rewrite works.
omerman May 3, 2025
d084a9e
remove console logs
omerman May 3, 2025
2a73c3d
fix unreadable condition + remove leftover log
omerman May 3, 2025
e346a38
bring back accidental deleted log
omerman May 3, 2025
27d0a87
tidy up before tests
omerman May 3, 2025
9eb188a
support rewrite for URL objects
omerman May 3, 2025
72c774f
minor rename
omerman May 3, 2025
f27ea4c
properly handle urls+pathnames+searchparams
omerman May 4, 2025
2a14d73
more readable loadedRoute handling. fixed params assignement issue
omerman May 4, 2025
071e42d
fix some types. WIP e2e tests - for some reason not working due to pa…
omerman May 4, 2025
fa67692
attempt to fix the rewrite in preview - no luck
omerman May 4, 2025
9059162
texts
omerman May 4, 2025
251d992
fix preview whitescreen.
omerman May 4, 2025
84a64d9
introduce canonicalUrl - to know the request's original url
omerman May 4, 2025
9edd885
everything works including e2e's. will write some more and we're good…
omerman May 4, 2025
bc4794e
everything works well.
omerman May 4, 2025
f49f450
bump
omerman May 4, 2025
c3165a4
docs
omerman May 4, 2025
418bf51
remove logs
omerman May 4, 2025
632684e
minor simplification
omerman May 4, 2025
72993a0
more self pr
omerman May 4, 2025
5c4c0aa
fixed it all, finally.
omerman May 5, 2025
12b2e38
api updated
omerman May 5, 2025
8766726
CR fixes
omerman May 6, 2025
cd8684c
minor docs change
omerman May 6, 2025
327ac40
minor docs change
omerman May 6, 2025
68d7ba2
CR fixes -2-
omerman May 6, 2025
b5c2386
CR fixes
omerman May 12, 2025
f300e0a
make it experimental
omerman May 20, 2025
2f7f0ee
Fix the FF issues.
omerman May 20, 2025
6952f27
re-add bad error description
omerman May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/dirty-dolls-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@builder.io/qwik-city': minor
---

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");

// Url will remain /articles/the-best-article-in-the-world, but under the hood,
// will render /articles/${artistId}
throw rewrite(`/articles/${artistId}`);
}
};
```

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1020,6 +1026,29 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
</td></tr>
<tr><td>

[originalUrl](#)

</td><td>

`readonly`

</td><td>

URL

</td><td>

The original HTTP request 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.

</td></tr>
<tr><td>

[params](#)

</td><td>
Expand Down Expand Up @@ -1312,6 +1341,25 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
</td></tr>
<tr><td>

[rewrite](#)

</td><td>

`readonly`

</td><td>

(pathname: string) =&gt; [RewriteMessage](#rewritemessage)

</td><td>

When called, qwik-city will execute the path's matching route flow.

The url in the browser will remain unchanged.

</td></tr>
<tr><td>

[send](#)

</td><td>
Expand Down Expand Up @@ -1462,6 +1510,76 @@ 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)

<table><thead><tr><th>

Constructor

</th><th>

Modifiers

</th><th>

Description

</th></tr></thead>
<tbody><tr><td>

[(constructor)(pathname)](#)

</td><td>

</td><td>

Constructs a new instance of the `RewriteMessage` class

</td></tr>
</tbody></table>

<table><thead><tr><th>

Property

</th><th>

Modifiers

</th><th>

Type

</th><th>

Description

</th></tr></thead>
<tbody><tr><td>

[pathname](#rewritemessage-pathname)

</td><td>

`readonly`

</td><td>

string

</td><td>

</td></tr>
</tbody></table>

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

## ServerError

```typescript
Expand Down
19 changes: 18 additions & 1 deletion packages/docs/src/routes/api/qwik-optimizer/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@
"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",
"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",
Expand All @@ -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<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>",
"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>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts",
"mdFile": "qwik.experimentalfeatures.md"
},
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/routes/api/qwik-optimizer/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ string

string

## enableRequestRewrite

## EntryStrategy

```typescript
Expand Down Expand Up @@ -369,6 +371,19 @@ Description
</th></tr></thead>
<tbody><tr><td>

enableRequestRewrite

</td><td>

`"enableRequestRewrite"`

</td><td>

**_(ALPHA)_** Enable request.rewrite()

</td></tr>
<tr><td>

noSPA

</td><td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
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/[email protected]"
import type { RequestHandler } from "@builder.io/qwik-city";

export const onRequest: RequestHandler = async ({ url, rewrite }) => {
const pattern = /^\/articles\/(.*)$/;
// Detects /articles/<article-name>, 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, is invoked with a pathname string.

```tsx
throw rewrite(`/articles/777/`);
```
18 changes: 18 additions & 0 deletions packages/qwik-city/src/buildtime/vite/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { matchRoute } from '../../runtime/src/route-matcher';
import { getMenuLoader } from '../../runtime/src/routing';
import type {
ActionInternal,
RebuildRouteInfoInternal,
ContentMenu,
LoadedRoute,
LoaderInternal,
Expand Down Expand Up @@ -227,10 +228,27 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) {
await server.ssrLoadModule('@qwik-serializer');
const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable };

const rebuildRouteInfo: RebuildRouteInfoInternal = 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,
rebuildRouteInfo,
ctx.opts.trailingSlash,
ctx.opts.basePathname,
qwikSerializer
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export interface RequestEventBase<PLATFORM = QwikCityPlatform> {
readonly env: EnvGetter;
readonly headers: Headers;
readonly method: string;
readonly originalUrl: URL;
readonly params: Readonly<Record<string, string>>;
readonly parseBody: () => Promise<unknown>;
readonly pathname: string;
Expand All @@ -134,6 +135,7 @@ export interface RequestEventCommon<PLATFORM = QwikCityPlatform> 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: (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
Expand Down Expand Up @@ -174,6 +176,13 @@ export interface ResolveValue {
<T>(action: Action<T>): Promise<T | undefined>;
}

// @public (undocumented)
export class RewriteMessage extends AbortMessage {
constructor(pathname: string);
// (undocumented)
readonly pathname: string;
}

// @public (undocumented)
export class ServerError<T = any> extends Error {
constructor(status: number, data: T);
Expand Down
Loading