Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/returned-control-flow-signals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/router': minor
---

feat: returning `ev.redirect()`, `ev.error()` or `ev.rewrite()` from a loader, action, request handler or server function now behaves the same as throwing them
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { component$ } from '@qwik.dev/core';
import { Form, routeAction$ } from '@qwik.dev/router';

// Returns the error signal instead of throwing it.
export const useErrorAction = routeAction$((_data, { error }) =>
error(403, 'returned-action-error')
);

export default component$(() => {
const action = useErrorAction();
return (
<Form action={action}>
<button type="submit">Submit</button>
</Form>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { component$ } from '@qwik.dev/core';
import type { RequestHandler } from '@qwik.dev/router';

// Returns the redirect signal from a request handler instead of throwing it.
export const onGet: RequestHandler = ({ redirect }) =>
redirect(302, '/qwikrouter-test/returned-control-flow/target/');

export default component$(() => {
return <h1 id="returned-control-flow-handler-redirect">Should not render</h1>;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { component$ } from '@qwik.dev/core';
import { routeLoader$ } from '@qwik.dev/router';

// Returns the error signal instead of throwing it.
export const useErrorLoader = routeLoader$(({ error }) => error(401, 'returned-loader-error'));

export default component$(() => {
useErrorLoader();
return <h1 id="returned-control-flow-loader-error">Should not render</h1>;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { component$ } from '@qwik.dev/core';
import { routeLoader$ } from '@qwik.dev/router';

// Returns the redirect signal instead of throwing it.
export const useRedirectLoader = routeLoader$(({ redirect }) =>
redirect(302, '/qwikrouter-test/returned-control-flow/target/')
);

export default component$(() => {
useRedirectLoader();
return <h1 id="returned-control-flow-loader-redirect">Should not render</h1>;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { component$ } from '@qwik.dev/core';

export default component$(() => {
return <h1 id="returned-control-flow-target">Target</h1>;
});
32 changes: 32 additions & 0 deletions e2e/qwik-e2e/tests/qwikrouter/returned-control-flow.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect, test } from '@playwright/test';

const base = '/qwikrouter-test/returned-control-flow';

// Returning ev.redirect()/ev.error() must behave the same as throwing them.
test.describe('returned control-flow signals', () => {
test('loader returning redirect redirects', async ({ page }) => {
await page.goto(`${base}/loader-redirect/`);
await expect(page.locator('#returned-control-flow-target')).toBeVisible();
});

test('request handler returning redirect redirects', async ({ page }) => {
await page.goto(`${base}/handler-redirect/`);
await expect(page.locator('#returned-control-flow-target')).toBeVisible();
});

test('loader returning error renders the error response', async ({ page }) => {
const response = await page.goto(`${base}/loader-error/`);
expect(response?.status()).toEqual(401);
await expect(page.locator('body')).toContainText('returned-loader-error');
});

test('action returning error responds with the error status', async ({ page }) => {
await page.goto(`${base}/action-error/`);
const actionPath = await page.locator('form').getAttribute('action');
const response = await page.request.post(new URL(actionPath!, page.url()).href, {
headers: { Accept: 'application/json' },
form: {},
});
expect(response.status()).toEqual(403);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
"id": "qwik-router-middleware-request-handler",
"package": "@qwik.dev/qwik-router/middleware/request-handler",
"members": [
{
"name": "__controlFlow",
"id": "abortmessage-__controlflow",
"hierarchy": [
{
"name": "AbortMessage",
"id": "abortmessage-__controlflow"
},
{
"name": "__controlFlow",
"id": "abortmessage-__controlflow"
}
],
"kind": "Property",
"content": "Type-only brand so `Exclude<>` can drop control-flow signals from resolved loader/action data.\n\n\n```typescript\nreadonly __controlFlow: true;\n```",
"mdFile": "router.abortmessage.__controlflow.md"
},
{
"name": "AbortMessage",
"id": "abortmessage",
Expand All @@ -12,7 +29,7 @@
}
],
"kind": "Class",
"content": "```typescript\nexport declare class AbortMessage \n```",
"content": "```typescript\nexport declare class AbortMessage \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\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\n[\\_\\_controlFlow](#abortmessage-__controlflow)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\ntrue\n\n\n</td><td>\n\nType-only brand so `Exclude<>` can drop control-flow signals from resolved loader/action data.\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/redirect-handler.ts",
"mdFile": "router.abortmessage.md"
},
Expand Down Expand Up @@ -414,7 +431,7 @@
}
],
"kind": "Function",
"content": "```typescript\nexport type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void> | void;\n```\n**References:** [RequestEvent](#requestevent)",
"content": "```typescript\nexport type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void | AbortMessage | ServerError> | void | AbortMessage | ServerError;\n```\n**References:** [RequestEvent](#requestevent)<!-- -->, [AbortMessage](#abortmessage)<!-- -->, [ServerError](#servererror)",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/request-handler.ts",
"mdFile": "router.requesthandler.md"
},
Expand All @@ -428,7 +445,7 @@
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void> | void;\n```\n**References:** [RequestEvent](#requestevent)",
"content": "```typescript\nexport type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void | AbortMessage | ServerError> | void | AbortMessage | ServerError;\n```\n**References:** [RequestEvent](#requestevent)<!-- -->, [AbortMessage](#abortmessage)<!-- -->, [ServerError](#servererror)",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts",
"mdFile": "router.requesthandler.md"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,56 @@ title: \@qwik.dev/qwik-router/middleware/request-handler API Reference

# [API](/api) &rsaquo; @qwik.dev/qwik-router/middleware/request-handler

<h2 id="abortmessage-__controlflow">__controlFlow</h2>

Type-only brand so `Exclude<>` can drop control-flow signals from resolved loader/action data.

```typescript
readonly __controlFlow: true;
```

<h2 id="abortmessage">AbortMessage</h2>

```typescript
export declare class AbortMessage
```

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

Property

</th><th>

Modifiers

</th><th>

Type

</th><th>

Description

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

[\_\_controlFlow](#abortmessage-__controlflow)

</td><td>

`readonly`

</td><td>

true

</td><td>

Type-only brand so `Exclude<>` can drop control-flow signals from resolved loader/action data.

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

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

<h2 id="cookie-append">append</h2>
Expand Down Expand Up @@ -1598,10 +1642,14 @@ resolveValue
```typescript
export type RequestHandler<PLATFORM = QwikRouterPlatform> = (
ev: RequestEvent<PLATFORM>,
) => Promise<void> | void;
) =>
| Promise<void | AbortMessage | ServerError>
| void
| AbortMessage
| ServerError;
```

**References:** [RequestEvent](#requestevent)
**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [ServerError](#servererror)

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

Expand All @@ -1610,10 +1658,14 @@ export type RequestHandler<PLATFORM = QwikRouterPlatform> = (
```typescript
export type RequestHandler<PLATFORM = QwikRouterPlatform> = (
ev: RequestEvent<PLATFORM>,
) => Promise<void> | void;
) =>
| Promise<void | AbortMessage | ServerError>
| void
| AbortMessage
| ServerError;
```

**References:** [RequestEvent](#requestevent)
**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [ServerError](#servererror)

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

Expand Down
18 changes: 16 additions & 2 deletions packages/docs/src/routes/api/qwik-router/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type Action<RETURN, INPUT = Record<string, unknown>, OPTIONAL extends boolean = true> = {\n (): ActionStore<RETURN, INPUT, OPTIONAL>;\n};\n```\n**References:** [ActionStore](#actionstore)",
"content": "```typescript\nexport type Action<RETURN, INPUT = Record<string, unknown>, OPTIONAL extends boolean = true> = {\n (): ActionStore<ExcludeControlFlow<RETURN>, INPUT, OPTIONAL>;\n};\n```\n**References:** [ActionStore](#actionstore)<!-- -->, [ExcludeControlFlow](#excludecontrolflow)",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts",
"mdFile": "router.action.md"
},
Expand Down Expand Up @@ -282,6 +282,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/error-boundary.tsx",
"mdFile": "router.errorboundary.md"
},
{
"name": "ExcludeControlFlow",
"id": "excludecontrolflow",
"hierarchy": [
{
"name": "ExcludeControlFlow",
"id": "excludecontrolflow"
}
],
"kind": "TypeAlias",
"content": "Drops control-flow signals (`ev.redirect()`<!-- -->, `ev.error()`<!-- -->, etc.) from a loader/action return type: those are thrown, not surfaced as data. `ev.fail()` is plain data and is kept.\n\n\n```typescript\nexport type ExcludeControlFlow<T> = Exclude<T, AbortMessage | ServerError>;\n```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts",
"mdFile": "router.excludecontrolflow.md"
},
{
"name": "FailOfRest",
"id": "failofrest",
Expand Down Expand Up @@ -502,7 +516,7 @@
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type Loader<RETURN> = {\n (): LoaderSignal<RETURN>;\n};\n```\n**References:** [LoaderSignal](#loadersignal)",
"content": "```typescript\nexport type Loader<RETURN> = {\n (): LoaderSignal<ExcludeControlFlow<RETURN>>;\n};\n```\n**References:** [LoaderSignal](#loadersignal)<!-- -->, [ExcludeControlFlow](#excludecontrolflow)",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts",
"mdFile": "router.loader_2.md"
},
Expand Down
18 changes: 14 additions & 4 deletions packages/docs/src/routes/api/qwik-router/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export type Action<
INPUT = Record<string, unknown>,
OPTIONAL extends boolean = true,
> = {
(): ActionStore<RETURN, INPUT, OPTIONAL>;
(): ActionStore<ExcludeControlFlow<RETURN>, INPUT, OPTIONAL>;
};
```

**References:** [ActionStore](#actionstore)
**References:** [ActionStore](#actionstore), [ExcludeControlFlow](#excludecontrolflow)

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts)

Expand Down Expand Up @@ -783,6 +783,16 @@ ErrorBoundary: import("@qwik.dev/core").Component<ErrorBoundaryProps>;

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/error-boundary.tsx)

<h2 id="excludecontrolflow">ExcludeControlFlow</h2>

Drops control-flow signals (`ev.redirect()`, `ev.error()`, etc.) from a loader/action return type: those are thrown, not surfaced as data. `ev.fail()` is plain data and is kept.

```typescript
export type ExcludeControlFlow<T> = Exclude<T, AbortMessage | ServerError>;
```

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts)

<h2 id="failofrest">FailOfRest</h2>

```typescript
Expand Down Expand Up @@ -1294,11 +1304,11 @@ _(Optional)_

```typescript
export type Loader<RETURN> = {
(): LoaderSignal<RETURN>;
(): LoaderSignal<ExcludeControlFlow<RETURN>>;
};
```

**References:** [LoaderSignal](#loadersignal)
**References:** [LoaderSignal](#loadersignal), [ExcludeControlFlow](#excludecontrolflow)

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from '../../../runtime/src/types';
import { RequestEvSharedActionId, type RequestEventInternal } from '../request-event-core';
import { IsQAction, QActionId } from '../request-path';
import { throwIfControlFlowSignal } from '../server-error';
import type { QRL } from '@qwik.dev/core';
import type { RequestEventBase } from '../types';

Expand Down Expand Up @@ -92,6 +93,7 @@ export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
action!.__qrl.call(requestEv, result.data as JSONObject, requestEv)
)
: await action.__qrl.call(requestEv, result.data as JSONObject, requestEv);
throwIfControlFlowSignal(actionResolved);
if (devMode) {
verifySerializable(actionResolved, action.__qrl);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { ValueOrPromise } from '@qwik.dev/core';

// @public (undocumented)
export class AbortMessage {
readonly __controlFlow: true;
}

// Warning: (ae-forgotten-export) The symbol "RequestEventInternal" needs to be exported by the entry point index.d.ts
Expand Down Expand Up @@ -170,7 +171,7 @@ export interface RequestEventLoader<PLATFORM = QwikRouterPlatform> extends Reque
}

// @public (undocumented)
export type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void> | void;
export type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void | AbortMessage | ServerError> | void | AbortMessage | ServerError;

// Warning: (ae-forgotten-export) The symbol "QwikRouterRun" needs to be exported by the entry point index.d.ts
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/** @public */
export class AbortMessage {}
export class AbortMessage {
/** Type-only brand so `Exclude<>` can drop control-flow signals from resolved loader/action data. */
declare readonly __controlFlow: true;
}
Comment on lines +2 to +5

/** @public */
export class RedirectMessage extends AbortMessage {}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from './request-path';
import { AbortMessage, RedirectMessage } from './redirect-handler';
import { RewriteMessage } from './rewrite-handler';
import { ServerError } from './server-error';
import { ServerError, throwIfControlFlowSignal } from './server-error';
import { encoder, getContentType } from './request-utils';
import type {
CacheControl,
Expand Down Expand Up @@ -92,9 +92,7 @@ export function createRequestEvent(
while (routeModuleIndex < requestHandlers.length) {
const moduleRequestHandler = requestHandlers[routeModuleIndex];
const result = moduleRequestHandler(requestEv);
if (isPromise(result)) {
await result;
}
throwIfControlFlowSignal(isPromise(result) ? await result : result);
routeModuleIndex++;
}
};
Expand Down
Loading
Loading