@antelopejs/dms-frontend 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -333,6 +333,11 @@ export interface DmsFrontendRuntime {
|
|
|
333
333
|
pendingNavigation?: Promise<void>;
|
|
334
334
|
hasNavigationListener: boolean;
|
|
335
335
|
pageVersion: number;
|
|
336
|
+
/**
|
|
337
|
+
* Identifies the page on screen (its path, or the error page shown at that
|
|
338
|
+
* path), whose meta `route.meta` holds.
|
|
339
|
+
*/
|
|
340
|
+
pageKey?: string;
|
|
336
341
|
/** The `Cookie` header of the request a server runtime renders. */
|
|
337
342
|
requestCookies?: string;
|
|
338
343
|
/**
|
|
@@ -495,7 +500,10 @@ function parseRoute(runtime: DmsFrontendRuntime, url: string): DmsRoute {
|
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
function updateRoute(runtime: DmsFrontendRuntime, url: string): void {
|
|
498
|
-
|
|
503
|
+
// The meta is the page's, not the URL's: a page declares it, then reads its
|
|
504
|
+
// route, and the middleware the meta selected must still find it.
|
|
505
|
+
const { meta: _pageMeta, ...location } = parseRoute(runtime, url);
|
|
506
|
+
Object.assign(runtime.route, location);
|
|
499
507
|
}
|
|
500
508
|
|
|
501
509
|
function locationToUrl(
|
|
@@ -578,16 +586,22 @@ async function visit(
|
|
|
578
586
|
export function useDmsRoute(pattern?: string): DmsRoute {
|
|
579
587
|
const runtime = useDmsRuntime();
|
|
580
588
|
runtime.currentRoutePattern = pattern;
|
|
589
|
+
// A server render answers one request, whose URL the runtime was given with
|
|
590
|
+
// its page, and never changes it. Inertia's page is one ref for the whole
|
|
591
|
+
// process, written by each render as it starts: after an await it may hold
|
|
592
|
+
// another request's URL, and while the plugins set up, the previous one's.
|
|
593
|
+
if (runtime.isServer) {
|
|
594
|
+
updateRoute(runtime, runtime.route.fullPath);
|
|
595
|
+
return runtime.route;
|
|
596
|
+
}
|
|
581
597
|
const page = usePage();
|
|
582
|
-
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
(url) => updateRoute(runtime, url),
|
|
590
|
-
);
|
|
598
|
+
// The browser's plugins set up before Inertia has a page: until then the
|
|
599
|
+
// runtime's route is the initial page's.
|
|
600
|
+
if (page.url !== undefined) updateRoute(runtime, page.url);
|
|
601
|
+
watch(
|
|
602
|
+
() => page.url,
|
|
603
|
+
(url) => updateRoute(runtime, url),
|
|
604
|
+
);
|
|
591
605
|
return runtime.route;
|
|
592
606
|
}
|
|
593
607
|
|
|
@@ -1360,6 +1374,14 @@ export function getDmsLayoutProps(
|
|
|
1360
1374
|
export function hydrateDmsPageProps(props: DmsPageProps, url?: string): void {
|
|
1361
1375
|
const runtime = useDmsRuntime();
|
|
1362
1376
|
if (url) updateRoute(runtime, url);
|
|
1377
|
+
// Pages are keyed by path: another path mounts another page, which declares
|
|
1378
|
+
// its own meta, and so does an error page shown in place of the page. A
|
|
1379
|
+
// visit that only changes the query keeps the page, and so its meta.
|
|
1380
|
+
const pageKey = props.error ? `error:${props.path}` : props.path;
|
|
1381
|
+
if (pageKey !== runtime.pageKey) {
|
|
1382
|
+
runtime.pageKey = pageKey;
|
|
1383
|
+
runtime.route.meta = {};
|
|
1384
|
+
}
|
|
1363
1385
|
runtime.pageVersion++;
|
|
1364
1386
|
runtime.currentError.value = null;
|
|
1365
1387
|
useDmsState<DmsUser | null>("dms-user", () => null).value =
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Answers for requests Node rejects before the request handler runs. Without
|
|
2
|
+
// a `clientError` listener Node answers on its own and logs nothing, so a page
|
|
3
|
+
// refused for its headers left no trace in the server output.
|
|
4
|
+
|
|
5
|
+
const HEADER_OVERFLOW = "HPE_HEADER_OVERFLOW";
|
|
6
|
+
const HEADER_OVERFLOW_RESPONSE =
|
|
7
|
+
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n";
|
|
8
|
+
const BAD_REQUEST_RESPONSE =
|
|
9
|
+
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
|
10
|
+
// A browser retries a refused page with the same cookies: one hint per minute
|
|
11
|
+
// is enough to explain them all.
|
|
12
|
+
const HEADER_OVERFLOW_HINT_INTERVAL_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
let lastHeaderOverflowHint = -Infinity;
|
|
15
|
+
|
|
16
|
+
function warnHeaderOverflow(maxHeaderSize) {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (now - lastHeaderOverflowHint < HEADER_OVERFLOW_HINT_INTERVAL_MS) return;
|
|
19
|
+
lastHeaderOverflowHint = now;
|
|
20
|
+
console.warn(
|
|
21
|
+
`DMS refused a request with 431: its headers exceed the ${maxHeaderSize}-byte limit. ` +
|
|
22
|
+
"This is most often cookies accumulated on this host (browsers share " +
|
|
23
|
+
"localhost cookies across ports); clear this site's cookies and reload.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The server's `clientError` listener: explains a 431 in the logs, and
|
|
29
|
+
* otherwise answers as Node does without a listener.
|
|
30
|
+
*/
|
|
31
|
+
export function handleClientError(error, socket, maxHeaderSize) {
|
|
32
|
+
let response = BAD_REQUEST_RESPONSE;
|
|
33
|
+
if (error.code === HEADER_OVERFLOW) {
|
|
34
|
+
response = HEADER_OVERFLOW_RESPONSE;
|
|
35
|
+
warnHeaderOverflow(maxHeaderSize);
|
|
36
|
+
}
|
|
37
|
+
// Once a response went out on this connection, a status line would land in
|
|
38
|
+
// the middle of it.
|
|
39
|
+
if (socket.writable && socket.bytesWritten === 0) socket.write(response);
|
|
40
|
+
socket.destroy();
|
|
41
|
+
}
|
package/templates/vue/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { createServer } from "node:http";
|
|
2
|
+
import { createServer, maxHeaderSize } from "node:http";
|
|
3
3
|
import { extname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
redirectAccessRefusal,
|
|
20
20
|
UNEXPECTED_ERROR_BODY,
|
|
21
21
|
} from "./server/backend-response.mjs";
|
|
22
|
+
import { handleClientError } from "./server/client-error.mjs";
|
|
22
23
|
import { productionHtmlTemplate } from "./server/client-manifest.mjs";
|
|
23
24
|
import { writeContent } from "./server/content.mjs";
|
|
24
25
|
import {
|
|
@@ -400,6 +401,22 @@ async function answeredByVite(devServer, request, response) {
|
|
|
400
401
|
return result.status !== "unhandled";
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Vite's own URLs (`/@vite/client`, `/@id/…`, `/@fs/…`) and paths with an
|
|
406
|
+
* extension name files: modules, stylesheets, fonts, images, built assets.
|
|
407
|
+
* The backend's routes outside `/api` and `/dms` are mostly the paths of the
|
|
408
|
+
* pages they serve data for, which have none (`/<page>/pagelayout`).
|
|
409
|
+
*/
|
|
410
|
+
function isFilePath(pathname) {
|
|
411
|
+
return pathname.startsWith("/@") || extname(pathname) !== "";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Vite's files in development, the build's in production. */
|
|
415
|
+
async function answeredWithFile(devServer, request, response, pathname) {
|
|
416
|
+
if (!devServer) return serveAsset(pathname, request, response);
|
|
417
|
+
return pathname !== "/" && answeredByVite(devServer, request, response);
|
|
418
|
+
}
|
|
419
|
+
|
|
403
420
|
export async function handleRequest(request, response) {
|
|
404
421
|
const url = new URL(request.url, "http://frontend.local");
|
|
405
422
|
const pathname = url.pathname;
|
|
@@ -420,13 +437,18 @@ export async function handleRequest(request, response) {
|
|
|
420
437
|
return proxy(request, response);
|
|
421
438
|
if (requestFailure.hasMalformedPath(request.url))
|
|
422
439
|
return requestFailure.writeBadRequest(response);
|
|
423
|
-
if (!isFrontendVisit(request) && (await proxy(request, response, true)))
|
|
424
|
-
return;
|
|
425
440
|
const devServer = await developmentServer();
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
441
|
+
const visit = isFrontendVisit(request);
|
|
442
|
+
// A file is looked for among the frontend's own before the backend is asked
|
|
443
|
+
// for it: asking first cost a backend round trip for each of the hundreds of
|
|
444
|
+
// modules a development page loads, and for each built asset. A file the
|
|
445
|
+
// frontend does not have, a media file, is still the backend's.
|
|
446
|
+
const fileFirst = !visit && isFilePath(pathname);
|
|
447
|
+
const serveFile = () =>
|
|
448
|
+
answeredWithFile(devServer, request, response, pathname);
|
|
449
|
+
if (fileFirst && (await serveFile())) return;
|
|
450
|
+
if (!visit && (await proxy(request, response, true))) return;
|
|
451
|
+
if (!fileFirst && (await serveFile())) return;
|
|
430
452
|
if (handleAssetVersionMismatch(request, response)) return;
|
|
431
453
|
if (redirectToHomepage(request, response, pathname)) return;
|
|
432
454
|
const props = await pageProps(request);
|
|
@@ -489,6 +511,9 @@ if (
|
|
|
489
511
|
import.meta.url === new URL(`file://${process.argv[1]}`).href
|
|
490
512
|
) {
|
|
491
513
|
frontendHttpServer = createServer(handleRequestSafely);
|
|
514
|
+
frontendHttpServer.on("clientError", (error, socket) =>
|
|
515
|
+
handleClientError(error, socket, maxHeaderSize),
|
|
516
|
+
);
|
|
492
517
|
frontendHttpServer.listen(
|
|
493
518
|
Number(process.env.PORT ?? 3001),
|
|
494
519
|
process.env.HOST ?? "0.0.0.0",
|