Skip to content

Commit 2cebbdb

Browse files
Varixowmertens
authored andcommitted
refactor: move handlers to separate files
1 parent 8f1ee26 commit 2cebbdb

14 files changed

+298
-312
lines changed

packages/qwik-router/src/buildtime/build-layout.unit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const test = testAppSuite('Build Layout');
44

55
test('total layouts', ({ ctx: { layouts } }) => {
66
// $ find starters/apps/qwikrouter-test/src/routes -name layout*tsx | wc -l
7-
assert.equal(layouts.length, 13, JSON.stringify(layouts, null, 2));
7+
assert.equal(layouts.length, 14, JSON.stringify(layouts, null, 2));
88
});
99

1010
test('nested named layout', ({ assertLayout }) => {

packages/qwik-router/src/middleware/request-handler/action-endpoints.ts renamed to packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@ import type {
33
JSONObject,
44
RequestEvent,
55
RequestHandler,
6-
} from '../../runtime/src/types';
7-
import { runValidators } from './loader-endpoints';
8-
import {
9-
getRequestActions,
10-
getRequestMode,
11-
RequestEvQwikSerializer,
12-
type RequestEventInternal,
13-
} from './request-event';
14-
import { measure, verifySerializable } from './resolve-request-handlers';
15-
import type { QwikSerializer } from './types';
16-
import { IsQAction, QActionId } from './user-response';
17-
import { _UNINITIALIZED, type ValueOrPromise } from '@qwik.dev/core/internal';
6+
} from '../../../runtime/src/types';
7+
import { runValidators } from './loader-handler';
8+
import { getRequestActions, getRequestMode, type RequestEventInternal } from '../request-event';
9+
import { measure, verifySerializable } from '../resolve-request-handlers';
10+
import { IsQAction, QActionId } from '../user-response';
11+
import { _serialize, _UNINITIALIZED, type ValueOrPromise } from '@qwik.dev/core/internal';
1812

1913
export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
2014
return async (requestEvent: RequestEvent) => {
@@ -33,7 +27,6 @@ export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
3327
// Execute just this action
3428
const actions = getRequestActions(requestEv);
3529
const isDev = getRequestMode(requestEv) === 'dev';
36-
const qwikSerializer = requestEv[RequestEvQwikSerializer];
3730
const method = requestEv.method;
3831

3932
if (isDev && method === 'GET') {
@@ -62,11 +55,11 @@ export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
6255
return;
6356
}
6457

65-
await executeAction(action, actions, requestEv, isDev, qwikSerializer);
58+
await executeAction(action, actions, requestEv, isDev);
6659

6760
if (requestEv.request.headers.get('accept')?.includes('application/json')) {
6861
// only return the action data if the client accepts json, otherwise return the html page
69-
const data = await qwikSerializer._serialize([actions[actionId]]);
62+
const data = await _serialize([actions[actionId]]);
7063
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
7164
requestEv.send(200, data);
7265
return;
@@ -79,8 +72,7 @@ async function executeAction(
7972
action: ActionInternal,
8073
actions: Record<string, ValueOrPromise<unknown> | undefined>,
8174
requestEv: RequestEventInternal,
82-
isDev: boolean,
83-
qwikSerializer: QwikSerializer
75+
isDev: boolean
8476
) {
8577
const selectedActionId = action.__id;
8678
requestEv.sharedMap.set(QActionId, selectedActionId);
@@ -98,7 +90,7 @@ async function executeAction(
9890
)
9991
: await action.__qrl.call(requestEv, result.data as JSONObject, requestEv);
10092
if (isDev) {
101-
verifySerializable(qwikSerializer, actionResolved, action.__qrl);
93+
verifySerializable(actionResolved, action.__qrl);
10294
}
10395
actions[selectedActionId] = actionResolved;
10496
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { RequestEvent } from '@qwik.dev/router/middleware/request-handler';
2+
3+
export function isContentType(headers: Headers, ...types: string[]) {
4+
const type = headers.get('content-type')?.split(/;/, 1)[0].trim() ?? '';
5+
return types.includes(type);
6+
}
7+
8+
export function csrfLaxProtoCheckMiddleware(requestEv: RequestEvent) {
9+
checkCSRF(requestEv, true);
10+
}
11+
export function csrfCheckMiddleware(requestEv: RequestEvent) {
12+
checkCSRF(requestEv);
13+
}
14+
function checkCSRF(requestEv: RequestEvent, laxProto?: true) {
15+
const isForm = isContentType(
16+
requestEv.request.headers,
17+
'application/x-www-form-urlencoded',
18+
'multipart/form-data',
19+
'text/plain'
20+
);
21+
if (isForm) {
22+
const inputOrigin = requestEv.request.headers.get('origin');
23+
const origin = requestEv.url.origin;
24+
let forbidden = inputOrigin !== origin;
25+
26+
if (
27+
forbidden &&
28+
laxProto &&
29+
inputOrigin?.replace(/^http(s)?/g, '') === origin.replace(/^http(s)?/g, '')
30+
) {
31+
forbidden = false;
32+
}
33+
34+
if (forbidden) {
35+
throw requestEv.error(
36+
403,
37+
`CSRF check failed. Cross-site ${requestEv.method} form submissions are forbidden.
38+
The request origin "${inputOrigin}" does not match the server origin "${origin}".`
39+
);
40+
}
41+
}
42+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { csrfCheckMiddleware, isContentType } from './csrf-handler';
3+
import { RequestEvent } from '@qwik.dev/router/middleware/request-handler';
4+
5+
describe('csrf handler', () => {
6+
it.each([
7+
{
8+
contentType: 'application/x-www-form-urlencoded',
9+
},
10+
{
11+
contentType: 'multipart/form-data',
12+
},
13+
{
14+
contentType: 'text/plain',
15+
},
16+
])('should throw an error if the origin does not match for $contentType', ({ contentType }) => {
17+
const errorFn = vi.fn();
18+
const requestEv = {
19+
request: {
20+
headers: new Headers({
21+
'content-type': contentType,
22+
origin: 'http://example.com',
23+
}),
24+
},
25+
url: new URL('http://bad-example.com'),
26+
error: errorFn,
27+
} as unknown as RequestEvent;
28+
29+
try {
30+
csrfCheckMiddleware(requestEv);
31+
} catch (_) {
32+
// ignore the error here, we just want to check the errorFn
33+
}
34+
35+
expect(errorFn).toBeCalledWith(403, expect.stringMatching('CSRF check failed'));
36+
});
37+
38+
describe('isContentType', () => {
39+
it('should correctly identify form/data', () => {
40+
const headers = new Headers({
41+
'content-type':
42+
'multipart/form-data; boundary=---------------------------5509475224001460121912752931',
43+
});
44+
expect(isContentType(headers, 'multipart/form-data')).toBe(true);
45+
});
46+
});
47+
});

packages/qwik-router/src/middleware/request-handler/loader-endpoints.ts renamed to packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
1+
import qwikRouterConfig from '@qwik-router-config';
12
import { _serialize, _UNINITIALIZED } from '@qwik.dev/core/internal';
23
import type {
34
DataValidator,
45
LoaderInternal,
56
RequestHandler,
67
ValidatorReturn,
7-
} from '../../runtime/src/types';
8+
} from '../../../runtime/src/types';
9+
import { getPathnameForDynamicRoute } from '../../../utils/pathname';
810
import {
911
getRequestLoaders,
1012
getRequestLoaderSerializationStrategyMap,
1113
getRequestMode,
1214
RequestEventInternal,
13-
} from './request-event';
14-
import { measure, verifySerializable } from './resolve-request-handlers';
15-
import type { RequestEvent } from './types';
16-
import { IsQLoader, IsQLoaderData, QLoaderId } from './user-response';
17-
import qwikRouterConfig from '@qwik-router-config';
18-
import { getPathnameForDynamicRoute } from '../../utils/pathname';
15+
} from '../request-event';
16+
import { measure, verifySerializable } from '../resolve-request-handlers';
17+
import type { RequestEvent } from '../types';
18+
import { IsQLoader, IsQLoaderData, QLoaderId } from '../user-response';
19+
20+
export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandler {
21+
return async (requestEvent: RequestEvent) => {
22+
const requestEv = requestEvent as RequestEventInternal;
23+
if (requestEv.headersSent) {
24+
requestEv.exit();
25+
return;
26+
}
27+
const loaders = getRequestLoaders(requestEv);
28+
const isDev = getRequestMode(requestEv) === 'dev';
29+
if (routeLoaders.length > 0) {
30+
const resolvedLoadersPromises = routeLoaders.map((loader) =>
31+
executeLoader(loader, loaders, requestEv, isDev)
32+
);
33+
await Promise.all(resolvedLoadersPromises);
34+
}
35+
};
36+
}
1937

2038
export function loaderDataHandler(routeLoaders: LoaderInternal[]): RequestHandler {
2139
return async (requestEvent: RequestEvent) => {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { RequestEvent } from '../types';
2+
import { isQDataRequestBasedOnSharedMap } from '../resolve-request-handlers';
3+
import { HttpStatus } from '../http-status-codes';
4+
5+
export function fixTrailingSlash(ev: RequestEvent) {
6+
const { basePathname, originalUrl, sharedMap } = ev;
7+
const { pathname, search } = originalUrl;
8+
const isQData = isQDataRequestBasedOnSharedMap(sharedMap);
9+
if (!isQData && pathname !== basePathname && !pathname.endsWith('.html')) {
10+
// only check for slash redirect on pages
11+
if (!globalThis.__NO_TRAILING_SLASH__) {
12+
// must have a trailing slash
13+
if (!pathname.endsWith('/')) {
14+
// add slash to existing pathname
15+
throw ev.redirect(HttpStatus.MovedPermanently, pathname + '/' + search);
16+
}
17+
} else {
18+
// should not have a trailing slash
19+
if (pathname.endsWith('/')) {
20+
// remove slash from existing pathname
21+
throw ev.redirect(
22+
HttpStatus.MovedPermanently,
23+
pathname.slice(0, pathname.length - 1) + search
24+
);
25+
}
26+
}
27+
}
28+
}

packages/qwik-router/src/middleware/request-handler/qdata-endpoints.ts renamed to packages/qwik-router/src/middleware/request-handler/handlers/qdata-handler.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
// requestEv.sharedMap.get(RequestEvSharedActionId)
2-
1+
import { _serialize } from '@qwik.dev/core/internal';
32
import type { RequestEvent } from '@qwik.dev/router';
4-
import { _serialize } from 'packages/qwik/core-internal';
5-
import { RequestEvIsRewrite } from './request-event';
6-
import { getPathname } from './resolve-request-handlers';
7-
import { IsQData } from './user-response';
3+
import { RequestEvIsRewrite } from '../request-event';
4+
import { getPathname } from '../resolve-request-handlers';
5+
import { IsQData } from '../user-response';
86

97
export interface QData {
108
status: number;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { RedirectMessage, RequestEvent } from '@qwik.dev/router/middleware/request-handler';
2+
import { OriginalQDataName } from '../user-response';
3+
import { isQDataRequestBasedOnSharedMap } from '../resolve-request-handlers';
4+
5+
export async function handleRedirect(requestEv: RequestEvent) {
6+
const isPageDataReq = isQDataRequestBasedOnSharedMap(requestEv.sharedMap);
7+
if (!isPageDataReq) {
8+
return;
9+
}
10+
11+
try {
12+
await requestEv.next();
13+
} catch (err) {
14+
if (!(err instanceof RedirectMessage)) {
15+
throw err;
16+
}
17+
}
18+
if (requestEv.headersSent) {
19+
return;
20+
}
21+
22+
const status = requestEv.status();
23+
const location = requestEv.headers.get('Location');
24+
const isRedirect = status >= 301 && status <= 308 && location;
25+
26+
if (isRedirect) {
27+
const adaptedLocation = makeQDataPath(location, requestEv.sharedMap);
28+
if (adaptedLocation) {
29+
requestEv.headers.set('Location', adaptedLocation);
30+
requestEv.getWritableStream().close();
31+
return;
32+
} else {
33+
requestEv.status(200);
34+
requestEv.headers.delete('Location');
35+
}
36+
}
37+
}
38+
39+
function makeQDataPath(href: string, sharedMap: Map<string, unknown>) {
40+
if (href.startsWith('/')) {
41+
const url = new URL(href, 'http://localhost');
42+
const pathname = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
43+
const append = sharedMap.get(OriginalQDataName) as string;
44+
45+
if (!append) {
46+
return undefined;
47+
}
48+
49+
return pathname + (append.startsWith('/') ? '' : '/') + append + url.search;
50+
} else {
51+
return undefined;
52+
}
53+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { RequestEvent, ServerError } from '@qwik.dev/router/middleware/request-handler';
2+
import { QFN_KEY } from '../../../runtime/src/constants';
3+
import { getRequestMode } from '../request-event';
4+
import type { ErrorCodes } from '../types';
5+
import { encoder, measure, verifySerializable } from '../resolve-request-handlers';
6+
import type { QRL } from '@qwik.dev/core';
7+
import { _serialize } from '@qwik.dev/core/internal';
8+
9+
function isAsyncIterator(obj: unknown): obj is AsyncIterable<unknown> {
10+
return obj ? typeof obj === 'object' && Symbol.asyncIterator in obj : false;
11+
}
12+
13+
const isQrl = (value: any): value is QRL => {
14+
return typeof value === 'function' && typeof value.getSymbol === 'function';
15+
};
16+
17+
export async function pureServerFunction(ev: RequestEvent) {
18+
const fn = ev.query.get(QFN_KEY);
19+
if (
20+
fn &&
21+
ev.request.headers.get('X-QRL') === fn &&
22+
ev.request.headers.get('Content-Type') === 'application/qwik-json'
23+
) {
24+
ev.exit();
25+
const isDev = getRequestMode(ev) === 'dev';
26+
const data = await ev.parseBody();
27+
if (Array.isArray(data)) {
28+
const [qrl, ...args] = data;
29+
if (isQrl(qrl) && qrl.getHash() === fn) {
30+
let result: unknown;
31+
try {
32+
if (isDev) {
33+
result = await measure(ev, `server_${qrl.getSymbol()}`, () =>
34+
(qrl as Function).apply(ev, args)
35+
);
36+
} else {
37+
result = await (qrl as Function).apply(ev, args);
38+
}
39+
} catch (err) {
40+
if (err instanceof ServerError) {
41+
throw ev.error(err.status as ErrorCodes, err.data);
42+
}
43+
throw ev.error(500, 'Invalid request');
44+
}
45+
if (isAsyncIterator(result)) {
46+
ev.headers.set('Content-Type', 'text/qwik-json-stream');
47+
const writable = ev.getWritableStream();
48+
const stream = writable.getWriter();
49+
for await (const item of result) {
50+
if (isDev) {
51+
verifySerializable(item, qrl);
52+
}
53+
const message = await _serialize([item]);
54+
if (ev.signal.aborted) {
55+
break;
56+
}
57+
await stream.write(encoder.encode(`${message}\n`));
58+
}
59+
stream.close();
60+
} else {
61+
verifySerializable(result, qrl);
62+
ev.headers.set('Content-Type', 'application/qwik-json');
63+
const message = await _serialize([result]);
64+
ev.send(200, message);
65+
}
66+
return;
67+
}
68+
}
69+
throw ev.error(500, 'Invalid request');
70+
}
71+
}

packages/qwik-router/src/middleware/request-handler/request-event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
RewriteMessage,
2020
ServerError,
2121
} from '@qwik.dev/router/middleware/request-handler';
22-
import { executeLoader } from './loader-endpoints';
22+
import { executeLoader } from './handlers/loader-handler';
2323
import { encoder } from './resolve-request-handlers';
2424
import type {
2525
CacheControl,

0 commit comments

Comments
 (0)