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

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5c547fd
WIP
omerman May 2, 2025
be72f32
handleRewrite WIP
omerman May 2, 2025
8efb129
distinguish between redirect and rewrite
omerman May 2, 2025
ac5adbd
header property Location is a specific header meant for redirects, re…
omerman May 2, 2025
afe1a3d
rewrite finally doesnt throw error > behaves as redirect if the statu…
omerman May 2, 2025
43187d2
WIP - rewrite somewhat works
omerman May 3, 2025
39c0c54
Rewrite works.
omerman May 3, 2025
39cc82d
remove console logs
omerman May 3, 2025
28c59da
fix unreadable condition + remove leftover log
omerman May 3, 2025
a98515f
bring back accidental deleted log
omerman May 3, 2025
366f33b
tidy up before tests
omerman May 3, 2025
61f07ac
support rewrite for URL objects
omerman May 3, 2025
ad2b06d
minor rename
omerman May 3, 2025
556b32c
properly handle urls+pathnames+searchparams
omerman May 4, 2025
d1d725a
more readable loadedRoute handling. fixed params assignement issue
omerman May 4, 2025
d851c5c
fix some types. WIP e2e tests - for some reason not working due to pa…
omerman May 4, 2025
6ce376e
attempt to fix the rewrite in preview - no luck
omerman May 4, 2025
7404d4c
texts
omerman May 4, 2025
c36d680
fix preview whitescreen.
omerman May 4, 2025
1ab9cae
introduce canonicalUrl - to know the request's original url
omerman May 4, 2025
a502b72
everything works including e2e's. will write some more and we're good…
omerman May 4, 2025
b7e8bce
everything works well.
omerman May 4, 2025
4746984
bump
omerman May 4, 2025
1b0efd5
docs
omerman May 4, 2025
5ac3b04
remove logs
omerman May 4, 2025
f700c12
minor simplification
omerman May 4, 2025
6808b8f
more self pr
omerman May 4, 2025
da1ab2d
fixed it all, finally.
omerman May 5, 2025
da63b81
api updated
omerman May 5, 2025
cbf5855
CR fixes
omerman May 6, 2025
a83dc6b
minor docs change
omerman May 6, 2025
dff5aa5
minor docs change
omerman May 6, 2025
9b9d4b0
CR fixes -2-
omerman May 6, 2025
980ef0d
CR fixes
omerman May 12, 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,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,
Expand Down Expand Up @@ -82,6 +84,18 @@ export function createRequestEvent(
}
};

const resetRoute = (
_loadedRoute: LoadedRoute | null,
_requestHandlers: RequestHandler<any>[],
_url = url
) => {
loadedRoute = _loadedRoute;
requestHandlers = _requestHandlers;
url.pathname = _url.pathname;
url.search = _url.search;
routeModuleIndex = -1;
};

const check = () => {
if (writableStream !== null) {
throw new Error('Response already sent');
Expand Down Expand Up @@ -133,17 +147,26 @@ 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] ?? {},
pathname: url.pathname,
originalUrl: new URL(url),
get params() {
return loadedRoute?.[1] ?? {};
},
get pathname() {
return url.pathname;
},
platform,
query: url.searchParams,
get query() {
return url.searchParams;
},
request,
url,
basePathname,
Expand All @@ -160,6 +183,8 @@ export function createRequestEvent(

next,

resetRoute,

exit,

cacheControl: (cacheControl: CacheControl, target: CacheControlTarget = 'Cache-Control') => {
Expand Down Expand Up @@ -220,6 +245,15 @@ export function createRequestEvent(
return new RedirectMessage();
},

rewrite: (pathname: string) => {
check();
if (pathname.startsWith('http')) {
throw new Error('Rewrite does not support absolute urls');
}
sharedMap.set(RequestEvIsRewrite, true);
return new RewriteMessage(pathname.replace(/\/+/g, '/'));
},

defer: (returnData) => {
return typeof returnData === 'function' ? returnData : () => returnData;
},
Expand Down Expand Up @@ -296,6 +330,19 @@ export interface RequestEventInternal extends RequestEvent, RequestEventLoader {
* @returns `true`, if `getWritableStream()` has already been called.
*/
isDirty(): boolean;

/**
* 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 of the route.
*/
resetRoute(
loadedRoute: LoadedRoute | null,
requestHandlers: RequestHandler<any>[],
url: URL
): void;
}

export function getRequestLoaders(requestEv: RequestEventCommon) {
Expand Down
Loading