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.
Files changed (57) hide show
  1. package/dist/assets/build/generate.js +4 -3
  2. package/dist/cli/add/index.js +1 -0
  3. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  4. package/dist/container/index.d.ts +3 -3
  5. package/dist/content/content-layer.js +3 -3
  6. package/dist/content/runtime.d.ts +1 -1
  7. package/dist/content/runtime.js +1 -0
  8. package/dist/content/vite-plugin-content-virtual-mod.js +27 -0
  9. package/dist/core/app/base.js +7 -15
  10. package/dist/core/app/prepare-response.d.ts +2 -3
  11. package/dist/core/app/prepare-response.js +1 -6
  12. package/dist/core/build/generate.js +1 -1
  13. package/dist/core/build/plugins/plugin-css.js +1 -0
  14. package/dist/core/config/schemas/base.d.ts +4 -4
  15. package/dist/core/config/schemas/base.js +4 -4
  16. package/dist/core/config/validate.js +10 -2
  17. package/dist/core/constants.d.ts +0 -41
  18. package/dist/core/constants.js +1 -18
  19. package/dist/core/dev/dev.js +1 -1
  20. package/dist/core/errors/default-handler.js +22 -8
  21. package/dist/core/fetch/fetch-state.d.ts +11 -0
  22. package/dist/core/fetch/fetch-state.js +21 -2
  23. package/dist/core/fetch/index.d.ts +8 -3
  24. package/dist/core/fetch/index.js +2 -2
  25. package/dist/core/i18n/handler.js +7 -13
  26. package/dist/core/messages/runtime.js +1 -1
  27. package/dist/core/middleware/astro-middleware.d.ts +19 -0
  28. package/dist/core/middleware/astro-middleware.js +43 -0
  29. package/dist/core/middleware/noop-middleware.js +0 -2
  30. package/dist/core/middleware/vite-plugin.d.ts +1 -0
  31. package/dist/core/middleware/vite-plugin.js +5 -1
  32. package/dist/core/pages/handler.d.ts +13 -0
  33. package/dist/core/pages/handler.js +35 -14
  34. package/dist/core/routing/handler.js +6 -8
  35. package/dist/core/routing/rewrite.js +2 -1
  36. package/dist/core/util/normalized-url.js +2 -5
  37. package/dist/core/util/pathname.d.ts +17 -10
  38. package/dist/core/util/pathname.js +14 -7
  39. package/dist/i18n/index.js +11 -9
  40. package/dist/runtime/server/endpoint.d.ts +2 -1
  41. package/dist/runtime/server/endpoint.js +4 -13
  42. package/dist/runtime/server/jsx.js +2 -1
  43. package/dist/runtime/server/render/head.js +2 -1
  44. package/dist/runtime/server/render/util.js +4 -0
  45. package/dist/types/public/config.d.ts +68 -13
  46. package/dist/virtual-modules/container.d.ts +1 -1
  47. package/dist/vite-plugin-head/index.js +5 -11
  48. package/dist/vite-plugin-hmr-reload/index.js +19 -6
  49. package/dist/vite-plugin-html/transform/slots.js +4 -1
  50. package/dist/vite-plugin-pages/pages.d.ts +11 -0
  51. package/dist/vite-plugin-pages/pages.js +1 -3
  52. package/package.json +12 -6
  53. package/templates/content/module.mjs +0 -1
  54. package/dist/core/head-propagation/hint.d.ts +0 -4
  55. package/dist/core/head-propagation/hint.js +0 -7
  56. package/dist/jsx/rehype.d.ts +0 -5
  57. 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
- const typeHeader = response.headers.get(ROUTE_TYPE_HEADER);
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 (typeHeader !== "page" && typeHeader !== "fallback") {
41
+ if (state.responseRouteType !== "page" && state.responseRouteType !== "fallback") {
48
42
  return response;
49
43
  }
50
- const url = new URL(state.request.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: typeHeader,
57
- isReroute: isReroute === "yes"
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
- prerenderedRes.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
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 = typeHeader === "fallback" ? 404 : response.status;
87
+ const effectiveStatus = state.responseRouteType === "fallback" ? 404 : response.status;
94
88
  const fallbackDecision = computeFallbackRoute({
95
89
  pathname: url.pathname,
96
90
  responseStatus: effectiveStatus,
@@ -269,7 +269,7 @@ function printHelp({
269
269
  message.push(
270
270
  linebreak(),
271
271
  ` ${bgGreen(black(` ${commandName} `))} ${green(
272
- `v${"7.0.0-beta.3"}`
272
+ `v${"7.0.0-beta.5"}`
273
273
  )} ${headline}`
274
274
  );
275
275
  }
@@ -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;
@@ -1,7 +1,5 @@
1
- import { NOOP_MIDDLEWARE_HEADER } from "../constants.js";
2
1
  const NOOP_MIDDLEWARE_FN = async (_ctx, next) => {
3
2
  const response = await next();
4
- response.headers.set(NOOP_MIDDLEWARE_HEADER, "true");
5
3
  return response;
6
4
  };
7
5
  export {
@@ -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.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}.`)) return;
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
- response.headers.set(ROUTE_TYPE_HEADER, "page");
45
+ state.responseRouteType = "page";
50
46
  if (state.routeData.route === "/404" || state.routeData.route === "/500") {
51
- response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
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
- return new Response(null, { status: 500, headers: { [ROUTE_TYPE_HEADER]: "fallback" } });
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 && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
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 = decodeURI(pathname);
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 { MultiLevelEncodingError, validateAndDecodePathname } from "./pathname.js";
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 (e) {
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
- * Error thrown when multi-level URL encoding is detected in a pathname.
3
- * This is a distinct error type so callers can handle it specifically
4
- * (e.g., returning a 400 response) rather than falling back to partial decoding.
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
- * Validates that a pathname is not multi-level encoded.
11
- * Detects if a pathname contains encoding that was encoded again (e.g., %2561dmin where %25 decodes to %).
12
- * This prevents double/triple encoding bypasses of security checks.
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 pathname to validate
15
- * @returns The decoded pathname if valid
16
- * @throws MultiLevelEncodingError if multi-level encoding is detected
17
- * @throws Error if the pathname contains invalid URL encoding
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("Multi-level URL encoding is not allowed");
3
+ super("URL encoding depth exceeded the maximum number of decode iterations");
4
4
  this.name = "MultiLevelEncodingError";
5
5
  }
6
6
  }
7
- const ENCODING_REGEX = /%25[0-9a-fA-F]{2}/;
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
- if (ENCODING_REGEX.test(decoded)) {
19
- throw new MultiLevelEncodingError();
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
  }
@@ -1,6 +1,10 @@
1
- import { appendForwardSlash, joinPaths } from "@astrojs/internal-helpers/path";
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 { REROUTE_DIRECTIVE_HEADER } from "../core/constants.js";
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
- if (response?.headers.get(REROUTE_DIRECTIVE_HEADER) === "no" && typeof fallback === "undefined") {
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, REROUTE_DIRECTIVE_HEADER } from "../../core/constants.js";
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
- try {
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
- content += styles.join("\n") + links.join("\n") + scripts.join("\n");
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