astro 7.0.0-beta.3 → 7.0.0-beta.5
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/dist/assets/build/generate.js +4 -3
- package/dist/cli/add/index.js +1 -0
- package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
- package/dist/container/index.d.ts +3 -3
- package/dist/content/content-layer.js +3 -3
- package/dist/content/runtime.d.ts +1 -1
- package/dist/content/runtime.js +1 -0
- package/dist/content/vite-plugin-content-virtual-mod.js +27 -0
- package/dist/core/app/base.js +7 -15
- package/dist/core/app/prepare-response.d.ts +2 -3
- package/dist/core/app/prepare-response.js +1 -6
- package/dist/core/build/generate.js +1 -1
- package/dist/core/build/plugins/plugin-css.js +1 -0
- package/dist/core/config/schemas/base.d.ts +4 -4
- package/dist/core/config/schemas/base.js +4 -4
- package/dist/core/config/validate.js +10 -2
- package/dist/core/constants.d.ts +0 -41
- package/dist/core/constants.js +1 -18
- package/dist/core/dev/dev.js +1 -1
- package/dist/core/errors/default-handler.js +22 -8
- package/dist/core/fetch/fetch-state.d.ts +11 -0
- package/dist/core/fetch/fetch-state.js +21 -2
- package/dist/core/fetch/index.d.ts +8 -3
- package/dist/core/fetch/index.js +2 -2
- package/dist/core/i18n/handler.js +7 -13
- package/dist/core/messages/runtime.js +1 -1
- package/dist/core/middleware/astro-middleware.d.ts +19 -0
- package/dist/core/middleware/astro-middleware.js +43 -0
- package/dist/core/middleware/noop-middleware.js +0 -2
- package/dist/core/middleware/vite-plugin.d.ts +1 -0
- package/dist/core/middleware/vite-plugin.js +5 -1
- package/dist/core/pages/handler.d.ts +13 -0
- package/dist/core/pages/handler.js +35 -14
- package/dist/core/routing/handler.js +6 -8
- package/dist/core/routing/rewrite.js +2 -1
- package/dist/core/util/normalized-url.js +2 -5
- package/dist/core/util/pathname.d.ts +17 -10
- package/dist/core/util/pathname.js +14 -7
- package/dist/i18n/index.js +11 -9
- package/dist/runtime/server/endpoint.d.ts +2 -1
- package/dist/runtime/server/endpoint.js +4 -13
- package/dist/runtime/server/jsx.js +2 -1
- package/dist/runtime/server/render/head.js +2 -1
- package/dist/runtime/server/render/util.js +4 -0
- package/dist/types/public/config.d.ts +68 -13
- package/dist/virtual-modules/container.d.ts +1 -1
- package/dist/vite-plugin-head/index.js +5 -11
- package/dist/vite-plugin-hmr-reload/index.js +19 -6
- package/dist/vite-plugin-html/transform/slots.js +4 -1
- package/dist/vite-plugin-pages/pages.d.ts +11 -0
- package/dist/vite-plugin-pages/pages.js +1 -3
- package/package.json +12 -6
- package/templates/content/module.mjs +0 -1
- package/dist/core/head-propagation/hint.d.ts +0 -4
- package/dist/core/head-propagation/hint.js +0 -7
- package/dist/jsx/rehype.d.ts +0 -5
- package/dist/jsx/rehype.js +0 -241
|
@@ -3,7 +3,6 @@ import { computeFallbackRoute } from "../../i18n/fallback.js";
|
|
|
3
3
|
import { I18nRouter } from "../../i18n/router.js";
|
|
4
4
|
import { PipelineFeatures } from "../base-pipeline.js";
|
|
5
5
|
import { shouldAppendForwardSlash } from "../build/util.js";
|
|
6
|
-
import { REROUTE_DIRECTIVE_HEADER, ROUTE_TYPE_HEADER } from "../constants.js";
|
|
7
6
|
class I18n {
|
|
8
7
|
#i18n;
|
|
9
8
|
#base;
|
|
@@ -36,25 +35,20 @@ class I18n {
|
|
|
36
35
|
async finalize(state, response) {
|
|
37
36
|
state.pipeline.usedFeatures |= PipelineFeatures.i18n;
|
|
38
37
|
const i18n = this.#i18n;
|
|
39
|
-
|
|
40
|
-
if (typeHeader) {
|
|
41
|
-
response.headers.delete(ROUTE_TYPE_HEADER);
|
|
42
|
-
}
|
|
43
|
-
const isReroute = response.headers.get(REROUTE_DIRECTIVE_HEADER);
|
|
44
|
-
if (isReroute === "no" && typeof i18n.fallback === "undefined") {
|
|
38
|
+
if (state.skipErrorReroute && typeof i18n.fallback === "undefined") {
|
|
45
39
|
return response;
|
|
46
40
|
}
|
|
47
|
-
if (
|
|
41
|
+
if (state.responseRouteType !== "page" && state.responseRouteType !== "fallback") {
|
|
48
42
|
return response;
|
|
49
43
|
}
|
|
50
|
-
const url =
|
|
44
|
+
const url = state.url;
|
|
51
45
|
const currentLocale = state.computeCurrentLocale();
|
|
52
46
|
const isPrerendered = state.routeData.prerender;
|
|
53
47
|
const routerContext = {
|
|
54
48
|
currentLocale,
|
|
55
49
|
currentDomain: url.hostname,
|
|
56
|
-
routeType:
|
|
57
|
-
isReroute:
|
|
50
|
+
routeType: state.responseRouteType,
|
|
51
|
+
isReroute: false
|
|
58
52
|
};
|
|
59
53
|
const routeDecision = this.#router.match(url.pathname, routerContext);
|
|
60
54
|
switch (routeDecision.type) {
|
|
@@ -74,7 +68,7 @@ class I18n {
|
|
|
74
68
|
status: 404,
|
|
75
69
|
headers: response.headers
|
|
76
70
|
});
|
|
77
|
-
|
|
71
|
+
state.skipErrorReroute = true;
|
|
78
72
|
if (routeDecision.location) {
|
|
79
73
|
prerenderedRes.headers.set("Location", routeDecision.location);
|
|
80
74
|
}
|
|
@@ -90,7 +84,7 @@ class I18n {
|
|
|
90
84
|
break;
|
|
91
85
|
}
|
|
92
86
|
if (i18n.fallback && i18n.fallbackType) {
|
|
93
|
-
const effectiveStatus =
|
|
87
|
+
const effectiveStatus = state.responseRouteType === "fallback" ? 404 : response.status;
|
|
94
88
|
const fallbackDecision = computeFallbackRoute({
|
|
95
89
|
pathname: url.pathname,
|
|
96
90
|
responseStatus: effectiveStatus,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FetchState } from '../fetch/fetch-state.js';
|
|
2
2
|
import type { APIContext } from '../../types/public/context.js';
|
|
3
|
+
import type { BaseApp } from '../app/base.js';
|
|
3
4
|
import { type Pipeline } from '../base-pipeline.js';
|
|
4
5
|
/**
|
|
5
6
|
* Callback invoked at the bottom of the middleware chain to dispatch the
|
|
@@ -24,4 +25,22 @@ export declare class AstroMiddleware {
|
|
|
24
25
|
#private;
|
|
25
26
|
constructor(pipeline: Pipeline);
|
|
26
27
|
handle(state: FetchState, renderRouteCallback: RenderRouteCallback): Promise<Response>;
|
|
28
|
+
/**
|
|
29
|
+
* Like `handle`, but mirrors the app-level error handling that
|
|
30
|
+
* `AstroHandler` provides on the standard path, the same way
|
|
31
|
+
* `PagesHandler.handleWithErrorFallback` does for `pages()`. When no
|
|
32
|
+
* route matched it returns a 404 marked with `X-Astro-Error` for the
|
|
33
|
+
* app's post-check; when Astro's own middleware chain throws it logs the
|
|
34
|
+
* error and renders the custom `500.astro`.
|
|
35
|
+
*
|
|
36
|
+
* Errors surfaced through `renderRouteCallback` (the host framework's
|
|
37
|
+
* `next`, e.g. host middleware mounted below `middleware()`) are
|
|
38
|
+
* re-thrown instead, so the host's own error handling still runs rather
|
|
39
|
+
* than being swallowed into Astro's 500 page. A sentinel tells the two
|
|
40
|
+
* apart.
|
|
41
|
+
*
|
|
42
|
+
* Used by the composable `astro/fetch` `middleware()` entry point, where
|
|
43
|
+
* there is no surrounding `AstroHandler` to supply this fallback.
|
|
44
|
+
*/
|
|
45
|
+
handleWithErrorFallback(app: BaseApp<Pipeline>, state: FetchState, renderRouteCallback: RenderRouteCallback): Promise<Response>;
|
|
27
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PipelineFeatures } from "../base-pipeline.js";
|
|
2
|
+
import { ASTRO_ERROR_HEADER } from "../constants.js";
|
|
2
3
|
import { attachCookiesToResponse } from "../cookies/index.js";
|
|
3
4
|
import { applyRewriteToState } from "../rewrites/handler.js";
|
|
4
5
|
import { callMiddleware } from "./callMiddleware.js";
|
|
@@ -41,6 +42,48 @@ class AstroMiddleware {
|
|
|
41
42
|
state.response = response;
|
|
42
43
|
return response;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Like `handle`, but mirrors the app-level error handling that
|
|
47
|
+
* `AstroHandler` provides on the standard path, the same way
|
|
48
|
+
* `PagesHandler.handleWithErrorFallback` does for `pages()`. When no
|
|
49
|
+
* route matched it returns a 404 marked with `X-Astro-Error` for the
|
|
50
|
+
* app's post-check; when Astro's own middleware chain throws it logs the
|
|
51
|
+
* error and renders the custom `500.astro`.
|
|
52
|
+
*
|
|
53
|
+
* Errors surfaced through `renderRouteCallback` (the host framework's
|
|
54
|
+
* `next`, e.g. host middleware mounted below `middleware()`) are
|
|
55
|
+
* re-thrown instead, so the host's own error handling still runs rather
|
|
56
|
+
* than being swallowed into Astro's 500 page. A sentinel tells the two
|
|
57
|
+
* apart.
|
|
58
|
+
*
|
|
59
|
+
* Used by the composable `astro/fetch` `middleware()` entry point, where
|
|
60
|
+
* there is no surrounding `AstroHandler` to supply this fallback.
|
|
61
|
+
*/
|
|
62
|
+
async handleWithErrorFallback(app, state, renderRouteCallback) {
|
|
63
|
+
if (!state.routeData) {
|
|
64
|
+
return new Response(null, { status: 404, headers: { [ASTRO_ERROR_HEADER]: "true" } });
|
|
65
|
+
}
|
|
66
|
+
let nextError;
|
|
67
|
+
try {
|
|
68
|
+
return await this.handle(state, async (s, ctx) => {
|
|
69
|
+
try {
|
|
70
|
+
return await renderRouteCallback(s, ctx);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
nextError = err;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err === nextError) throw err;
|
|
78
|
+
app.logger.error(null, err.stack || err.message || String(err));
|
|
79
|
+
return app.renderError(state.request, {
|
|
80
|
+
...state.renderOptions,
|
|
81
|
+
status: 500,
|
|
82
|
+
error: err,
|
|
83
|
+
pathname: state.pathname
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
44
87
|
#finalize(state, response) {
|
|
45
88
|
attachCookiesToResponse(response, state.cookies);
|
|
46
89
|
return response;
|
|
@@ -3,6 +3,7 @@ import type { AstroSettings } from '../../types/astro.js';
|
|
|
3
3
|
import type { BuildInternals } from '../build/internal.js';
|
|
4
4
|
import type { StaticBuildOptions } from '../build/types.js';
|
|
5
5
|
export declare const MIDDLEWARE_MODULE_ID = "virtual:astro:middleware";
|
|
6
|
+
export declare function isMiddlewarePath(relativePath: string): boolean;
|
|
6
7
|
export declare function vitePluginMiddleware({ settings }: {
|
|
7
8
|
settings: AstroSettings;
|
|
8
9
|
}): VitePlugin;
|
|
@@ -11,6 +11,9 @@ import { normalizePath } from "../viteUtils.js";
|
|
|
11
11
|
const MIDDLEWARE_MODULE_ID = "virtual:astro:middleware";
|
|
12
12
|
const MIDDLEWARE_RESOLVED_MODULE_ID = "\0" + MIDDLEWARE_MODULE_ID;
|
|
13
13
|
const NOOP_MIDDLEWARE = "\0noop-middleware";
|
|
14
|
+
function isMiddlewarePath(relativePath) {
|
|
15
|
+
return relativePath.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}.`) || relativePath.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}/`);
|
|
16
|
+
}
|
|
14
17
|
function vitePluginMiddleware({ settings }) {
|
|
15
18
|
let resolvedMiddlewareId = void 0;
|
|
16
19
|
const hasIntegrationMiddleware = settings.middlewares.pre.length > 0 || settings.middlewares.post.length > 0;
|
|
@@ -26,7 +29,7 @@ function vitePluginMiddleware({ settings }) {
|
|
|
26
29
|
const normalizedPath = viteNormalizePath(path);
|
|
27
30
|
if (!normalizedPath.startsWith(normalizedSrcDir)) return;
|
|
28
31
|
const relativePath = normalizedPath.slice(normalizedSrcDir.length);
|
|
29
|
-
if (!relativePath
|
|
32
|
+
if (!isMiddlewarePath(relativePath)) return;
|
|
30
33
|
for (const name of [
|
|
31
34
|
ASTRO_VITE_ENVIRONMENT_NAMES.ssr,
|
|
32
35
|
ASTRO_VITE_ENVIRONMENT_NAMES.astro
|
|
@@ -135,6 +138,7 @@ function vitePluginMiddlewareBuild(opts, internals) {
|
|
|
135
138
|
}
|
|
136
139
|
export {
|
|
137
140
|
MIDDLEWARE_MODULE_ID,
|
|
141
|
+
isMiddlewarePath,
|
|
138
142
|
vitePluginMiddleware,
|
|
139
143
|
vitePluginMiddlewareBuild
|
|
140
144
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { APIContext } from '../../types/public/context.js';
|
|
2
|
+
import type { BaseApp } from '../app/base.js';
|
|
2
3
|
import type { FetchState } from '../fetch/fetch-state.js';
|
|
3
4
|
import type { Pipeline } from '../base-pipeline.js';
|
|
4
5
|
/**
|
|
@@ -17,4 +18,16 @@ export declare class PagesHandler {
|
|
|
17
18
|
#private;
|
|
18
19
|
constructor(pipeline: Pipeline);
|
|
19
20
|
handle(state: FetchState, ctx: APIContext): Promise<Response>;
|
|
21
|
+
/**
|
|
22
|
+
* Like `handle`, but mirrors the app-level error handling that
|
|
23
|
+
* `AstroHandler` provides on the standard path: unmatched routes
|
|
24
|
+
* return a 404 marked with `X-Astro-Error` for the app's post-check
|
|
25
|
+
* to render the 404 error page, and render-time errors are logged
|
|
26
|
+
* and render the 500 error page instead of propagating to the host
|
|
27
|
+
* framework.
|
|
28
|
+
*
|
|
29
|
+
* Used by the composable `astro/fetch` `pages()` entry point, where
|
|
30
|
+
* there is no surrounding `AstroHandler` to supply this fallback.
|
|
31
|
+
*/
|
|
32
|
+
handleWithErrorFallback(app: BaseApp<Pipeline>, state: FetchState): Promise<Response>;
|
|
20
33
|
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { renderEndpoint } from "../../runtime/server/endpoint.js";
|
|
2
2
|
import { renderPage } from "../../runtime/server/index.js";
|
|
3
|
-
import {
|
|
4
|
-
ASTRO_ERROR_HEADER,
|
|
5
|
-
REROUTE_DIRECTIVE_HEADER,
|
|
6
|
-
REWRITE_DIRECTIVE_HEADER_KEY,
|
|
7
|
-
REWRITE_DIRECTIVE_HEADER_VALUE,
|
|
8
|
-
ROUTE_TYPE_HEADER
|
|
9
|
-
} from "../constants.js";
|
|
3
|
+
import { ASTRO_ERROR_HEADER } from "../constants.js";
|
|
10
4
|
import { getCookiesFromResponse } from "../cookies/response.js";
|
|
11
5
|
const EMPTY_SLOTS = Object.freeze({});
|
|
12
6
|
class PagesHandler {
|
|
@@ -17,6 +11,7 @@ class PagesHandler {
|
|
|
17
11
|
async handle(state, ctx) {
|
|
18
12
|
const pipeline = this.#pipeline;
|
|
19
13
|
const { logger, streaming } = pipeline;
|
|
14
|
+
state.resetResponseMetadata();
|
|
20
15
|
let response;
|
|
21
16
|
const componentInstance = await state.loadComponentInstance();
|
|
22
17
|
switch (state.routeData.type) {
|
|
@@ -25,7 +20,8 @@ class PagesHandler {
|
|
|
25
20
|
componentInstance,
|
|
26
21
|
ctx,
|
|
27
22
|
state.routeData.prerender,
|
|
28
|
-
logger
|
|
23
|
+
logger,
|
|
24
|
+
state
|
|
29
25
|
);
|
|
30
26
|
break;
|
|
31
27
|
}
|
|
@@ -46,12 +42,9 @@ class PagesHandler {
|
|
|
46
42
|
result.cancelled = true;
|
|
47
43
|
throw e;
|
|
48
44
|
}
|
|
49
|
-
|
|
45
|
+
state.responseRouteType = "page";
|
|
50
46
|
if (state.routeData.route === "/404" || state.routeData.route === "/500") {
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
if (state.isRewriting) {
|
|
54
|
-
response.headers.set(REWRITE_DIRECTIVE_HEADER_KEY, REWRITE_DIRECTIVE_HEADER_VALUE);
|
|
47
|
+
state.skipErrorReroute = true;
|
|
55
48
|
}
|
|
56
49
|
break;
|
|
57
50
|
}
|
|
@@ -59,7 +52,8 @@ class PagesHandler {
|
|
|
59
52
|
return new Response(null, { status: 404, headers: { [ASTRO_ERROR_HEADER]: "true" } });
|
|
60
53
|
}
|
|
61
54
|
case "fallback": {
|
|
62
|
-
|
|
55
|
+
state.responseRouteType = "fallback";
|
|
56
|
+
return new Response(null, { status: 500 });
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
const responseCookies = getCookiesFromResponse(response);
|
|
@@ -69,6 +63,33 @@ class PagesHandler {
|
|
|
69
63
|
state.response = response;
|
|
70
64
|
return response;
|
|
71
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Like `handle`, but mirrors the app-level error handling that
|
|
68
|
+
* `AstroHandler` provides on the standard path: unmatched routes
|
|
69
|
+
* return a 404 marked with `X-Astro-Error` for the app's post-check
|
|
70
|
+
* to render the 404 error page, and render-time errors are logged
|
|
71
|
+
* and render the 500 error page instead of propagating to the host
|
|
72
|
+
* framework.
|
|
73
|
+
*
|
|
74
|
+
* Used by the composable `astro/fetch` `pages()` entry point, where
|
|
75
|
+
* there is no surrounding `AstroHandler` to supply this fallback.
|
|
76
|
+
*/
|
|
77
|
+
async handleWithErrorFallback(app, state) {
|
|
78
|
+
if (!state.routeData) {
|
|
79
|
+
return new Response(null, { status: 404, headers: { [ASTRO_ERROR_HEADER]: "true" } });
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return await this.handle(state, state.getAPIContext());
|
|
83
|
+
} catch (err) {
|
|
84
|
+
app.logger.error(null, err.stack || err.message || String(err));
|
|
85
|
+
return app.renderError(state.request, {
|
|
86
|
+
...state.renderOptions,
|
|
87
|
+
status: 500,
|
|
88
|
+
error: err,
|
|
89
|
+
pathname: state.pathname
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
}
|
|
73
94
|
export {
|
|
74
95
|
PagesHandler
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { ActionHandler } from "../../actions/handler.js";
|
|
2
|
-
import {
|
|
3
|
-
REROUTABLE_STATUS_CODES,
|
|
4
|
-
REROUTE_DIRECTIVE_HEADER,
|
|
5
|
-
REWRITE_DIRECTIVE_HEADER_KEY
|
|
6
|
-
} from "../constants.js";
|
|
2
|
+
import { REROUTABLE_STATUS_CODES } from "../constants.js";
|
|
7
3
|
import { TrailingSlashHandler } from "./trailing-slash-handler.js";
|
|
8
4
|
import { CacheHandler, provideCache } from "../cache/handler.js";
|
|
9
5
|
import { I18n } from "../i18n/handler.js";
|
|
@@ -65,6 +61,9 @@ class AstroHandler {
|
|
|
65
61
|
}
|
|
66
62
|
async handle(state) {
|
|
67
63
|
state.pipeline.usedFeatures |= ALL_PIPELINE_FEATURES;
|
|
64
|
+
if (state.invalidEncoding) {
|
|
65
|
+
return new Response(null, { status: 400, statusText: "Bad Request" });
|
|
66
|
+
}
|
|
68
67
|
const trailingSlashRedirect = this.#trailingSlashHandler.handle(state);
|
|
69
68
|
if (trailingSlashRedirect) {
|
|
70
69
|
return trailingSlashRedirect;
|
|
@@ -128,12 +127,11 @@ class AstroHandler {
|
|
|
128
127
|
};
|
|
129
128
|
response = await this.#cacheHandler.handle(state, runPipeline);
|
|
130
129
|
}
|
|
131
|
-
const isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY);
|
|
132
130
|
this.#app.logThisRequest({
|
|
133
131
|
pathname,
|
|
134
132
|
method: request.method,
|
|
135
133
|
statusCode: response.status,
|
|
136
|
-
isRewrite,
|
|
134
|
+
isRewrite: state.isRewriting,
|
|
137
135
|
timeStart: state.timeStart
|
|
138
136
|
});
|
|
139
137
|
} catch (err) {
|
|
@@ -150,7 +148,7 @@ class AstroHandler {
|
|
|
150
148
|
}
|
|
151
149
|
if (REROUTABLE_STATUS_CODES.includes(response.status) && // If the body isn't null, that means the user sets the 404 status
|
|
152
150
|
// but uses the current route to handle the 404
|
|
153
|
-
response.body === null &&
|
|
151
|
+
response.body === null && !state.skipErrorReroute) {
|
|
154
152
|
return this.#app.renderError(request, {
|
|
155
153
|
...state.renderOptions,
|
|
156
154
|
response,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
trimSlashes
|
|
11
11
|
} from "../path.js";
|
|
12
12
|
import { createRequest } from "../request.js";
|
|
13
|
+
import { validateAndDecodePathname } from "../util/pathname.js";
|
|
13
14
|
import { DEFAULT_404_ROUTE } from "./internal/astro-designed-error-pages.js";
|
|
14
15
|
import { isRoute404, isRoute500 } from "./internal/route-errors.js";
|
|
15
16
|
function findRouteToRewrite({
|
|
@@ -36,7 +37,7 @@ function findRouteToRewrite({
|
|
|
36
37
|
buildFormat
|
|
37
38
|
);
|
|
38
39
|
newUrl.pathname = resolvedUrlPathname;
|
|
39
|
-
const decodedPathname =
|
|
40
|
+
const decodedPathname = validateAndDecodePathname(pathname);
|
|
40
41
|
if (isRoute404(decodedPathname)) {
|
|
41
42
|
const errorRoute = routes.find((route) => route.route === "/404");
|
|
42
43
|
if (errorRoute) {
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { collapseDuplicateSlashes } from "@astrojs/internal-helpers/path";
|
|
2
|
-
import {
|
|
2
|
+
import { validateAndDecodePathname } from "./pathname.js";
|
|
3
3
|
function createNormalizedUrl(requestUrl) {
|
|
4
4
|
return normalizeUrl(new URL(requestUrl));
|
|
5
5
|
}
|
|
6
6
|
function normalizeUrl(url) {
|
|
7
7
|
try {
|
|
8
8
|
url.pathname = validateAndDecodePathname(url.pathname);
|
|
9
|
-
} catch
|
|
10
|
-
if (e instanceof MultiLevelEncodingError) {
|
|
11
|
-
throw e;
|
|
12
|
-
}
|
|
9
|
+
} catch {
|
|
13
10
|
try {
|
|
14
11
|
url.pathname = decodeURI(url.pathname);
|
|
15
12
|
} catch {
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Thrown when a URL path is encoded so many times that we give up decoding it
|
|
3
|
+
* (see {@link validateAndDecodePathname}). When this happens we reject the
|
|
4
|
+
* request with a `400` instead of guessing the path. If we let a half-decoded
|
|
5
|
+
* path through, your middleware might check one path while Astro routes to a
|
|
6
|
+
* different one.
|
|
5
7
|
*/
|
|
6
8
|
export declare class MultiLevelEncodingError extends Error {
|
|
7
9
|
constructor();
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Decodes a URL path over and over until it stops changing, so a path that was
|
|
13
|
+
* encoded several times ends up as a single, final path. This stops someone
|
|
14
|
+
* from sneaking a path like `/admin` past middleware by encoding it multiple
|
|
15
|
+
* times — middleware always sees the real, decoded path.
|
|
13
16
|
*
|
|
14
|
-
* @param pathname - The
|
|
15
|
-
* @returns The
|
|
16
|
-
* @throws
|
|
17
|
-
*
|
|
17
|
+
* @param pathname - The path to decode
|
|
18
|
+
* @returns The final, fully decoded path
|
|
19
|
+
* @throws Error if the path has broken encoding that can't be decoded at all
|
|
20
|
+
* (for example a lone `%` that isn't followed by two hex digits)
|
|
21
|
+
* @throws MultiLevelEncodingError if the path is still changing after
|
|
22
|
+
* {@link MAX_DECODE_ITERATIONS} tries (it was encoded too many times).
|
|
23
|
+
* Handing back a half-decoded path here would bring back the security hole
|
|
24
|
+
* this function exists to close.
|
|
18
25
|
*/
|
|
19
26
|
export declare function validateAndDecodePathname(pathname: string): string;
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
class MultiLevelEncodingError extends Error {
|
|
2
2
|
constructor() {
|
|
3
|
-
super("
|
|
3
|
+
super("URL encoding depth exceeded the maximum number of decode iterations");
|
|
4
4
|
this.name = "MultiLevelEncodingError";
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
-
const
|
|
7
|
+
const MAX_DECODE_ITERATIONS = 10;
|
|
8
8
|
function validateAndDecodePathname(pathname) {
|
|
9
|
-
if (ENCODING_REGEX.test(pathname)) {
|
|
10
|
-
throw new MultiLevelEncodingError();
|
|
11
|
-
}
|
|
12
9
|
let decoded;
|
|
13
10
|
try {
|
|
14
11
|
decoded = decodeURI(pathname);
|
|
15
12
|
} catch (_e) {
|
|
16
13
|
throw new Error("Invalid URL encoding");
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
let iterations = 0;
|
|
16
|
+
while (decoded !== pathname) {
|
|
17
|
+
if (iterations >= MAX_DECODE_ITERATIONS) {
|
|
18
|
+
throw new MultiLevelEncodingError();
|
|
19
|
+
}
|
|
20
|
+
pathname = decoded;
|
|
21
|
+
try {
|
|
22
|
+
decoded = decodeURI(pathname);
|
|
23
|
+
} catch {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
iterations++;
|
|
20
27
|
}
|
|
21
28
|
return decoded;
|
|
22
29
|
}
|
package/dist/i18n/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendForwardSlash,
|
|
3
|
+
joinPaths,
|
|
4
|
+
removeTrailingForwardSlash
|
|
5
|
+
} from "@astrojs/internal-helpers/path";
|
|
2
6
|
import { shouldAppendForwardSlash } from "../core/build/util.js";
|
|
3
|
-
import {
|
|
7
|
+
import { getFetchStateFromAPIContext } from "../core/fetch/fetch-state.js";
|
|
4
8
|
import { i18nNoLocaleFoundInPath, MissingLocale } from "../core/errors/errors-data.js";
|
|
5
9
|
import { AstroError } from "../core/errors/index.js";
|
|
6
10
|
import { createI18nMiddleware } from "./middleware.js";
|
|
@@ -55,7 +59,7 @@ function getLocaleRelativeUrl({
|
|
|
55
59
|
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
|
56
60
|
relativePath = appendForwardSlash(joinPaths(...pathsToJoin));
|
|
57
61
|
} else {
|
|
58
|
-
relativePath = joinPaths(...pathsToJoin);
|
|
62
|
+
relativePath = removeTrailingForwardSlash(joinPaths(...pathsToJoin));
|
|
59
63
|
}
|
|
60
64
|
if (relativePath === "") {
|
|
61
65
|
return "/";
|
|
@@ -196,24 +200,22 @@ function redirectToDefaultLocale({
|
|
|
196
200
|
}
|
|
197
201
|
function notFound({ base, locales, fallback }) {
|
|
198
202
|
return function(context, response) {
|
|
199
|
-
|
|
203
|
+
const fetchState = getFetchStateFromAPIContext(context);
|
|
204
|
+
if (fetchState.skipErrorReroute && typeof fallback === "undefined") {
|
|
200
205
|
return response;
|
|
201
206
|
}
|
|
202
207
|
const url = context.url;
|
|
203
208
|
const isRoot = url.pathname === base + "/" || url.pathname === base;
|
|
204
209
|
if (!(isRoot || pathHasLocale(url.pathname, locales))) {
|
|
210
|
+
fetchState.skipErrorReroute = true;
|
|
205
211
|
if (response) {
|
|
206
|
-
response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
|
|
207
212
|
return new Response(response.body, {
|
|
208
213
|
status: 404,
|
|
209
214
|
headers: response.headers
|
|
210
215
|
});
|
|
211
216
|
} else {
|
|
212
217
|
return new Response(null, {
|
|
213
|
-
status: 404
|
|
214
|
-
headers: {
|
|
215
|
-
[REROUTE_DIRECTIVE_HEADER]: "no"
|
|
216
|
-
}
|
|
218
|
+
status: 404
|
|
217
219
|
});
|
|
218
220
|
}
|
|
219
221
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { FetchState } from '../../core/fetch/fetch-state.js';
|
|
1
2
|
import type { AstroLogger } from '../../core/logger/core.js';
|
|
2
3
|
import type { APIRoute } from '../../types/public/common.js';
|
|
3
4
|
import type { APIContext } from '../../types/public/context.js';
|
|
4
5
|
/** Renders an endpoint request to completion, returning the body. */
|
|
5
6
|
export declare function renderEndpoint(mod: {
|
|
6
7
|
[method: string]: APIRoute;
|
|
7
|
-
}, context: APIContext, isPrerendered: boolean, logger: AstroLogger): Promise<Response>;
|
|
8
|
+
}, context: APIContext, isPrerendered: boolean, logger: AstroLogger, state?: FetchState): Promise<Response>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import colors from "piccolore";
|
|
2
|
-
import { REROUTABLE_STATUS_CODES
|
|
2
|
+
import { REROUTABLE_STATUS_CODES } from "../../core/constants.js";
|
|
3
3
|
import { AstroError } from "../../core/errors/errors.js";
|
|
4
4
|
import { EndpointDidNotReturnAResponse } from "../../core/errors/errors-data.js";
|
|
5
|
-
async function renderEndpoint(mod, context, isPrerendered, logger) {
|
|
5
|
+
async function renderEndpoint(mod, context, isPrerendered, logger, state) {
|
|
6
6
|
const { request, url } = context;
|
|
7
7
|
const method = request.method.toUpperCase();
|
|
8
8
|
let handler = mod[method] ?? mod["ALL"];
|
|
@@ -38,17 +38,8 @@ Found handlers: ${Object.keys(mod).map((exp) => JSON.stringify(exp)).join(", ")}
|
|
|
38
38
|
if (!response || response instanceof Response === false) {
|
|
39
39
|
throw new AstroError(EndpointDidNotReturnAResponse);
|
|
40
40
|
}
|
|
41
|
-
if (REROUTABLE_STATUS_CODES.includes(response.status)) {
|
|
42
|
-
|
|
43
|
-
response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
|
|
44
|
-
} catch (err) {
|
|
45
|
-
if (err.message?.includes("immutable")) {
|
|
46
|
-
response = new Response(response.body, response);
|
|
47
|
-
response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
|
|
48
|
-
} else {
|
|
49
|
-
throw err;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
41
|
+
if (state && REROUTABLE_STATUS_CODES.includes(response.status)) {
|
|
42
|
+
state.skipErrorReroute = true;
|
|
52
43
|
}
|
|
53
44
|
if (method === "HEAD") {
|
|
54
45
|
return new Response(null, response);
|
|
@@ -87,7 +87,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
|
|
87
87
|
_slots.default.push(child);
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
|
-
if ("slot" in child.props) {
|
|
90
|
+
if ("slot" in child.props && !isCustomElement) {
|
|
91
91
|
_slots[child.props.slot] = [..._slots[child.props.slot] ?? [], child];
|
|
92
92
|
delete child.props.slot;
|
|
93
93
|
return;
|
|
@@ -116,6 +116,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
|
|
116
116
|
const _slots = {
|
|
117
117
|
default: []
|
|
118
118
|
};
|
|
119
|
+
const isCustomElement = typeof vnode.type === "string" && vnode.type.includes("-");
|
|
119
120
|
extractSlots2(children);
|
|
120
121
|
for (const [key, value] of Object.entries(props)) {
|
|
121
122
|
if (value?.["$$slot"]) {
|
|
@@ -51,7 +51,8 @@ function renderAllHeadContent(result) {
|
|
|
51
51
|
const links = deduplicateElements(Array.from(result.links)).map(
|
|
52
52
|
(link) => renderElement("link", link, false)
|
|
53
53
|
);
|
|
54
|
-
|
|
54
|
+
const sep = result.compressHTML === true || result.compressHTML === "jsx" ? "" : "\n";
|
|
55
|
+
content += styles.join(sep) + links.join(sep) + scripts.join(sep);
|
|
55
56
|
content += result._metadata.extraHead.join("");
|
|
56
57
|
return markHTMLString(content);
|
|
57
58
|
}
|
|
@@ -6,6 +6,7 @@ const htmlBooleanAttributes = /^(?:allowfullscreen|async|autofocus|autoplay|chec
|
|
|
6
6
|
const AMPERSAND_REGEX = /&/g;
|
|
7
7
|
const DOUBLE_QUOTE_REGEX = /"/g;
|
|
8
8
|
const STATIC_DIRECTIVES = /* @__PURE__ */ new Set(["set:html", "set:text"]);
|
|
9
|
+
const INVALID_ATTR_NAME_CHAR = /[\s"'>/=]/;
|
|
9
10
|
const toIdent = (k) => k.trim().replace(/(?!^)\b\w|\s+|\W+/g, (match, index) => {
|
|
10
11
|
if (/\W/.test(match)) return "";
|
|
11
12
|
return index === 0 ? match : match.toUpperCase();
|
|
@@ -43,6 +44,9 @@ function addAttribute(value, key, shouldEscape = true, tagName = "") {
|
|
|
43
44
|
if (value == null) {
|
|
44
45
|
return "";
|
|
45
46
|
}
|
|
47
|
+
if (INVALID_ATTR_NAME_CHAR.test(key)) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
46
50
|
if (STATIC_DIRECTIVES.has(key)) {
|
|
47
51
|
console.warn(`[astro] The "${key}" directive cannot be applied dynamically at runtime. It will not be rendered as an attribute.
|
|
48
52
|
|