astro 7.0.0-beta.4 → 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 (36) hide show
  1. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  2. package/dist/content/content-layer.js +3 -3
  3. package/dist/core/app/prepare-response.d.ts +2 -3
  4. package/dist/core/app/prepare-response.js +1 -6
  5. package/dist/core/build/generate.js +1 -1
  6. package/dist/core/config/schemas/base.d.ts +1 -1
  7. package/dist/core/config/schemas/base.js +1 -1
  8. package/dist/core/constants.d.ts +0 -41
  9. package/dist/core/constants.js +1 -18
  10. package/dist/core/dev/dev.js +1 -1
  11. package/dist/core/errors/default-handler.js +1 -0
  12. package/dist/core/fetch/fetch-state.d.ts +11 -0
  13. package/dist/core/fetch/fetch-state.js +19 -1
  14. package/dist/core/fetch/index.d.ts +8 -3
  15. package/dist/core/fetch/index.js +2 -2
  16. package/dist/core/i18n/handler.js +7 -13
  17. package/dist/core/messages/runtime.js +1 -1
  18. package/dist/core/middleware/astro-middleware.d.ts +19 -0
  19. package/dist/core/middleware/astro-middleware.js +43 -0
  20. package/dist/core/middleware/noop-middleware.js +0 -2
  21. package/dist/core/pages/handler.d.ts +13 -0
  22. package/dist/core/pages/handler.js +35 -14
  23. package/dist/core/routing/handler.js +6 -8
  24. package/dist/core/routing/rewrite.js +2 -1
  25. package/dist/core/util/pathname.d.ts +17 -16
  26. package/dist/core/util/pathname.js +6 -2
  27. package/dist/i18n/index.js +5 -7
  28. package/dist/runtime/server/endpoint.d.ts +2 -1
  29. package/dist/runtime/server/endpoint.js +4 -13
  30. package/dist/types/public/config.d.ts +6 -6
  31. package/dist/virtual-modules/container.d.ts +1 -1
  32. package/dist/vite-plugin-head/index.js +5 -11
  33. package/package.json +6 -6
  34. package/templates/content/module.mjs +0 -1
  35. package/dist/core/head-propagation/hint.d.ts +0 -4
  36. package/dist/core/head-propagation/hint.js +0 -7
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "7.0.0-beta.4";
3
+ version = "7.0.0-beta.5";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -197,7 +197,7 @@ ${contentConfig.error.message}`
197
197
  logger.info("Content config changed");
198
198
  shouldClear = true;
199
199
  }
200
- if (previousAstroVersion && previousAstroVersion !== "7.0.0-beta.4") {
200
+ if (previousAstroVersion && previousAstroVersion !== "7.0.0-beta.5") {
201
201
  logger.info("Astro version changed");
202
202
  shouldClear = true;
203
203
  }
@@ -205,8 +205,8 @@ ${contentConfig.error.message}`
205
205
  logger.info("Clearing content store");
206
206
  this.#store.clearAll();
207
207
  }
208
- if ("7.0.0-beta.4") {
209
- this.#store.metaStore().set("astro-version", "7.0.0-beta.4");
208
+ if ("7.0.0-beta.5") {
209
+ this.#store.metaStore().set("astro-version", "7.0.0-beta.5");
210
210
  }
211
211
  if (currentConfigDigest) {
212
212
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Strips internal-only headers from the response before sending it to the
3
- * user agent, and optionally appends cookies written via `Astro.cookie.set()`
4
- * to the `Set-Cookie` header.
2
+ * Appends cookies written via `Astro.cookie.set()` to the `Set-Cookie` header
3
+ * and marks the response as sent.
5
4
  *
6
5
  * This is a pure function with no dependencies on the app; it is shared by
7
6
  * `AstroHandler` and the various error handlers.
@@ -1,11 +1,6 @@
1
- import { INTERNAL_RESPONSE_HEADERS, responseSentSymbol } from "../constants.js";
1
+ import { responseSentSymbol } from "../constants.js";
2
2
  import { getSetCookiesFromResponse } from "../cookies/index.js";
3
3
  function prepareResponse(response, { addCookieHeader }) {
4
- for (const headerName of INTERNAL_RESPONSE_HEADERS) {
5
- if (response.headers.has(headerName)) {
6
- response.headers.delete(headerName);
7
- }
8
- }
9
4
  if (addCookieHeader) {
10
5
  for (const setCookieHeaderValue of getSetCookiesFromResponse(response)) {
11
6
  response.headers.append("set-cookie", setCookieHeaderValue);
@@ -267,7 +267,7 @@ async function renderPath({
267
267
  relativeLocation: locationSite,
268
268
  from: fromPath
269
269
  });
270
- if (config.compressHTML === true) {
270
+ if (config.compressHTML) {
271
271
  body = body.replaceAll("\n", "");
272
272
  }
273
273
  if (route.type !== "redirect") {
@@ -49,7 +49,7 @@ export declare const ASTRO_CONFIG_DEFAULTS: {
49
49
  devToolbar: {
50
50
  enabled: true;
51
51
  };
52
- compressHTML: true;
52
+ compressHTML: "jsx";
53
53
  server: {
54
54
  host: false;
55
55
  port: number;
@@ -38,7 +38,7 @@ const ASTRO_CONFIG_DEFAULTS = {
38
38
  devToolbar: {
39
39
  enabled: true
40
40
  },
41
- compressHTML: true,
41
+ compressHTML: "jsx",
42
42
  server: {
43
43
  host: false,
44
44
  port: 4321,
@@ -1,46 +1,5 @@
1
1
  export declare const ASTRO_VERSION: string;
2
2
  export declare const ASTRO_GENERATOR: string;
3
- /**
4
- * The name for the header used to help rerouting behavior.
5
- * When set to "no", astro will NOT try to reroute an error response to the corresponding error page, which is the default behavior that can sometimes lead to loops.
6
- *
7
- * ```ts
8
- * const response = new Response("keep this content as-is", {
9
- * status: 404,
10
- * headers: {
11
- * // note that using a variable name as the key of an object needs to be wrapped in square brackets in javascript
12
- * // without them, the header name will be interpreted as "REROUTE_DIRECTIVE_HEADER" instead of "X-Astro-Reroute"
13
- * [REROUTE_DIRECTIVE_HEADER]: 'no',
14
- * }
15
- * })
16
- * ```
17
- * Alternatively...
18
- * ```ts
19
- * response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no');
20
- * ```
21
- */
22
- export declare const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
23
- /**
24
- * Header and value that are attached to a Response object when a **user rewrite** occurs.
25
- *
26
- * This metadata is used to determine the origin of a Response. If a rewrite has occurred, it should be prioritised over other logic.
27
- */
28
- export declare const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
29
- export declare const REWRITE_DIRECTIVE_HEADER_VALUE = "yes";
30
- /**
31
- * This header is set by the no-op Astro middleware.
32
- */
33
- export declare const NOOP_MIDDLEWARE_HEADER = "X-Astro-Noop";
34
- /**
35
- * The name for the header used to help i18n middleware, which only needs to act on "page" and "fallback" route types.
36
- */
37
- export declare const ROUTE_TYPE_HEADER = "X-Astro-Route-Type";
38
- /**
39
- * Internal headers that should be stripped from the response before
40
- * sending it to the user agent. Add new internal headers here so
41
- * `prepareResponse` removes them automatically.
42
- */
43
- export declare const INTERNAL_RESPONSE_HEADERS: readonly ["X-Astro-Reroute", "X-Astro-Rewrite", "X-Astro-Noop", "X-Astro-Route-Type"];
44
3
  /**
45
4
  * Set by internal handlers (e.g. PagesHandler) to signal that a
46
5
  * response should be replaced with the corresponding error page.
@@ -1,16 +1,5 @@
1
- const ASTRO_VERSION = "7.0.0-beta.4";
1
+ const ASTRO_VERSION = "7.0.0-beta.5";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
- const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
4
- const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
5
- const REWRITE_DIRECTIVE_HEADER_VALUE = "yes";
6
- const NOOP_MIDDLEWARE_HEADER = "X-Astro-Noop";
7
- const ROUTE_TYPE_HEADER = "X-Astro-Route-Type";
8
- const INTERNAL_RESPONSE_HEADERS = [
9
- REROUTE_DIRECTIVE_HEADER,
10
- REWRITE_DIRECTIVE_HEADER_KEY,
11
- NOOP_MIDDLEWARE_HEADER,
12
- ROUTE_TYPE_HEADER
13
- ];
14
3
  const ASTRO_ERROR_HEADER = "X-Astro-Error";
15
4
  const DEFAULT_404_COMPONENT = "astro-default-404.astro";
16
5
  const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308, 300, 304];
@@ -57,15 +46,9 @@ export {
57
46
  ASTRO_VERSION,
58
47
  ASTRO_VITE_ENVIRONMENT_NAMES,
59
48
  DEFAULT_404_COMPONENT,
60
- INTERNAL_RESPONSE_HEADERS,
61
49
  MIDDLEWARE_PATH_SEGMENT_NAME,
62
- NOOP_MIDDLEWARE_HEADER,
63
50
  REDIRECT_STATUS_CODES,
64
51
  REROUTABLE_STATUS_CODES,
65
- REROUTE_DIRECTIVE_HEADER,
66
- REWRITE_DIRECTIVE_HEADER_KEY,
67
- REWRITE_DIRECTIVE_HEADER_VALUE,
68
- ROUTE_TYPE_HEADER,
69
52
  SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
70
53
  appSymbol,
71
54
  clientAddressSymbol,
@@ -26,7 +26,7 @@ async function dev(inlineConfig) {
26
26
  await telemetry.record([]);
27
27
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
28
28
  const logger = restart.container.logger;
29
- const currentVersion = "7.0.0-beta.4";
29
+ const currentVersion = "7.0.0-beta.5";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -79,6 +79,7 @@ class DefaultErrorHandler {
79
79
  return this.renderError(request, {
80
80
  ...resolvedRenderOptions,
81
81
  status,
82
+ error,
82
83
  response: originalResponse,
83
84
  skipMiddleware: true,
84
85
  pathname: resolvedPathname
@@ -108,6 +108,12 @@ export declare class FetchState implements AstroFetchState {
108
108
  status: number;
109
109
  /** Whether user middleware should be skipped for this request. */
110
110
  skipMiddleware: boolean;
111
+ /**
112
+ * Set to `true` when the request path was encoded too many times to fully
113
+ * decode (see {@link validateAndDecodePathname}). These requests are
114
+ * rejected with a `400` before middleware or routing run.
115
+ */
116
+ invalidEncoding: boolean;
111
117
  /** A flag that tells the render content if the rewriting was triggered. */
112
118
  isRewriting: boolean;
113
119
  /** A safety net in case of loops (rewrite counter). */
@@ -122,6 +128,10 @@ export declare class FetchState implements AstroFetchState {
122
128
  clientAddress: string | undefined;
123
129
  /** Whether this is a partial render (container API). */
124
130
  partial: boolean | undefined;
131
+ /** Internal metadata about the current response route type. */
132
+ responseRouteType: 'page' | 'fallback' | undefined;
133
+ /** Internal flag to prevent rerouting this response to an error page. */
134
+ skipErrorReroute: boolean;
125
135
  /** Whether to inject CSP meta tags. */
126
136
  shouldInjectCspMetaTags: boolean | undefined;
127
137
  /** Request-scoped locals object, shared with user middleware. */
@@ -227,4 +237,5 @@ export declare class FetchState implements AstroFetchState {
227
237
  * after an in-flight rewrite swaps the route / request / params.
228
238
  */
229
239
  invalidateContexts(): void;
240
+ resetResponseMetadata(): void;
230
241
  }
@@ -29,7 +29,7 @@ import { getParams, getProps } from "../render/index.js";
29
29
  import { Rewrites } from "../rewrites/handler.js";
30
30
  import { isRoute404or500, isRouteServerIsland } from "../routing/match.js";
31
31
  import { normalizeUrl } from "../util/normalized-url.js";
32
- import { validateAndDecodePathname } from "../util/pathname.js";
32
+ import { MultiLevelEncodingError, validateAndDecodePathname } from "../util/pathname.js";
33
33
  import { getOriginPathname, setOriginPathname } from "../routing/rewrite.js";
34
34
  import { computePathnameFromDomain } from "../i18n/domain.js";
35
35
  import { getCustom404Route, routeHasHtmlExtension } from "../routing/helpers.js";
@@ -88,6 +88,12 @@ class FetchState {
88
88
  status = 200;
89
89
  /** Whether user middleware should be skipped for this request. */
90
90
  skipMiddleware = false;
91
+ /**
92
+ * Set to `true` when the request path was encoded too many times to fully
93
+ * decode (see {@link validateAndDecodePathname}). These requests are
94
+ * rejected with a `400` before middleware or routing run.
95
+ */
96
+ invalidEncoding = false;
91
97
  /** A flag that tells the render content if the rewriting was triggered. */
92
98
  isRewriting = false;
93
99
  /** A safety net in case of loops (rewrite counter). */
@@ -111,6 +117,10 @@ class FetchState {
111
117
  clientAddress;
112
118
  /** Whether this is a partial render (container API). */
113
119
  partial;
120
+ /** Internal metadata about the current response route type. */
121
+ responseRouteType;
122
+ /** Internal flag to prevent rerouting this response to an error page. */
123
+ skipErrorReroute = false;
114
124
  /** Whether to inject CSP meta tags. */
115
125
  shouldInjectCspMetaTags;
116
126
  /** Request-scoped locals object, shared with user middleware. */
@@ -697,6 +707,10 @@ class FetchState {
697
707
  try {
698
708
  return validateAndDecodePathname(pathname);
699
709
  } catch (e) {
710
+ if (e instanceof MultiLevelEncodingError) {
711
+ this.invalidEncoding = true;
712
+ return pathname;
713
+ }
700
714
  this.pipeline.logger.error(null, e.toString());
701
715
  return pathname;
702
716
  }
@@ -876,6 +890,10 @@ class FetchState {
876
890
  this.actionApiContext = null;
877
891
  this.apiContext = null;
878
892
  }
893
+ resetResponseMetadata() {
894
+ this.responseRouteType = void 0;
895
+ this.skipErrorReroute = false;
896
+ }
879
897
  }
880
898
  export {
881
899
  FetchState,
@@ -13,13 +13,18 @@ export declare function astro(state: FetchState): Promise<Response>;
13
13
  export declare function trailingSlash(state: FetchState): Response | undefined;
14
14
  /**
15
15
  * Runs Astro's middleware chain for the given state, calling `next` at
16
- * the bottom of the chain to produce the response. Lazily creates
17
- * the render context if needed.
16
+ * the bottom of the chain to produce the response. Lazily creates the
17
+ * render context if needed. Unmatched routes render the 404 error page;
18
+ * errors thrown by user middleware are logged and render the 500 error
19
+ * page; errors surfaced through `next` (the host framework's downstream
20
+ * chain) propagate to the host instead.
18
21
  */
19
22
  export declare function middleware(state: FetchState, next: (state: FetchState) => Promise<Response>): Promise<Response>;
20
23
  /**
21
24
  * Dispatches the request to the matched route (endpoint, page, redirect,
22
- * or fallback). Lazily creates the render context if needed.
25
+ * or fallback). Lazily creates the render context if needed. Unmatched
26
+ * routes render the 404 error page; render-time errors are logged and
27
+ * render the 500 error page.
23
28
  */
24
29
  export declare function pages(state: FetchState): Promise<Response>;
25
30
  /**
@@ -51,7 +51,7 @@ function middleware(state, next) {
51
51
  mw = new AstroMiddleware(app.pipeline);
52
52
  middlewareInstances.set(app, mw);
53
53
  }
54
- return mw.handle(state, (s, _ctx) => next(s));
54
+ return mw.handleWithErrorFallback(app, state, (s, _ctx) => next(s));
55
55
  }
56
56
  const pagesHandlers = /* @__PURE__ */ new WeakMap();
57
57
  function pages(state) {
@@ -61,7 +61,7 @@ function pages(state) {
61
61
  handler = new PagesHandler(app.pipeline);
62
62
  pagesHandlers.set(app, handler);
63
63
  }
64
- return handler.handle(state, state.getAPIContext());
64
+ return handler.handleWithErrorFallback(app, state);
65
65
  }
66
66
  function sessions(state) {
67
67
  return provideSession(state);
@@ -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.4"}`
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 {
@@ -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,25 +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.
5
- *
6
- * @deprecated No longer thrown internally — multi-level encoding is now
7
- * decoded iteratively instead of rejected. Kept for backwards compatibility
8
- * in case third-party code references the class.
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.
9
7
  */
10
8
  export declare class MultiLevelEncodingError extends Error {
11
9
  constructor();
12
10
  }
13
11
  /**
14
- * Decodes a pathname iteratively until stable, collapsing all levels of
15
- * percent-encoding into a single canonical form. This prevents
16
- * double/triple encoding from bypassing middleware authorization checks
17
- * (CVE-2025-66202)instead of rejecting multi-level encoding, we
18
- * fully resolve it so middleware always sees the true decoded path.
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
+ * timesmiddleware always sees the real, decoded path.
19
16
  *
20
- * @param pathname - The pathname to decode
21
- * @returns The fully decoded pathname
22
- * @throws Error if the pathname contains invalid URL encoding that
23
- * cannot be decoded at all (e.g., a bare `%` not followed by hex digits)
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.
24
25
  */
25
26
  export declare function validateAndDecodePathname(pathname: string): string;
@@ -1,9 +1,10 @@
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 MAX_DECODE_ITERATIONS = 10;
7
8
  function validateAndDecodePathname(pathname) {
8
9
  let decoded;
9
10
  try {
@@ -12,7 +13,10 @@ function validateAndDecodePathname(pathname) {
12
13
  throw new Error("Invalid URL encoding");
13
14
  }
14
15
  let iterations = 0;
15
- while (decoded !== pathname && iterations < 10) {
16
+ while (decoded !== pathname) {
17
+ if (iterations >= MAX_DECODE_ITERATIONS) {
18
+ throw new MultiLevelEncodingError();
19
+ }
16
20
  pathname = decoded;
17
21
  try {
18
22
  decoded = decodeURI(pathname);
@@ -4,7 +4,7 @@ import {
4
4
  removeTrailingForwardSlash
5
5
  } from "@astrojs/internal-helpers/path";
6
6
  import { shouldAppendForwardSlash } from "../core/build/util.js";
7
- import { REROUTE_DIRECTIVE_HEADER } from "../core/constants.js";
7
+ import { getFetchStateFromAPIContext } from "../core/fetch/fetch-state.js";
8
8
  import { i18nNoLocaleFoundInPath, MissingLocale } from "../core/errors/errors-data.js";
9
9
  import { AstroError } from "../core/errors/index.js";
10
10
  import { createI18nMiddleware } from "./middleware.js";
@@ -200,24 +200,22 @@ function redirectToDefaultLocale({
200
200
  }
201
201
  function notFound({ base, locales, fallback }) {
202
202
  return function(context, response) {
203
- if (response?.headers.get(REROUTE_DIRECTIVE_HEADER) === "no" && typeof fallback === "undefined") {
203
+ const fetchState = getFetchStateFromAPIContext(context);
204
+ if (fetchState.skipErrorReroute && typeof fallback === "undefined") {
204
205
  return response;
205
206
  }
206
207
  const url = context.url;
207
208
  const isRoot = url.pathname === base + "/" || url.pathname === base;
208
209
  if (!(isRoot || pathHasLocale(url.pathname, locales))) {
210
+ fetchState.skipErrorReroute = true;
209
211
  if (response) {
210
- response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
211
212
  return new Response(response.body, {
212
213
  status: 404,
213
214
  headers: response.headers
214
215
  });
215
216
  } else {
216
217
  return new Response(null, {
217
- status: 404,
218
- headers: {
219
- [REROUTE_DIRECTIVE_HEADER]: "no"
220
- }
218
+ status: 404
221
219
  });
222
220
  }
223
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);
@@ -420,22 +420,22 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
420
420
  * @docs
421
421
  * @name compressHTML
422
422
  * @type {boolean | "jsx"}
423
- * @default `true`
423
+ * @default `'jsx'`
424
424
  * @description
425
425
  *
426
426
  * Controls how Astro handles whitespace in your HTML. This affects both development mode and the final build output.
427
427
  *
428
- * By default, Astro removes whitespace from your HTML, including line breaks, in a lossless manner from `.astro` components. Some whitespace may be preserved as needed to maintain the visual rendering of your HTML.
428
+ * Since v7.0, Astro applies by default the JSX whitespace rules used by frameworks like React. This removes whitespace and line breaks around elements, collapses multi-line text onto a single line, and preserves whitespace within a single line (e.g. a space between two inline elements). To keep a space that would otherwise be removed, include it explicitly in the source through constructs such as `{" "}`.
429
429
  *
430
- * Since 6.2.0, this option can also be set to `"jsx"`, Astro will apply the JSX whitespace stripping rules used by frameworks like React. Leading and trailing whitespace is only preserved when explicitly included in the source code through constructs such as `{" "}`, and is otherwise removed entirely.
430
+ * Setting this option to `true` instead removes whitespace, including line breaks, in a lossless manner from `.astro` components. Some whitespace may be preserved as needed to maintain the visual rendering of your HTML.
431
431
  *
432
- * Setting this option to false disables HTML compression and preserves all whitespace.
432
+ * Setting this option to `false` disables HTML compression and preserves all whitespace.
433
433
  *
434
434
  * ```js
435
435
  * {
436
- * compressHTML: false
436
+ * compressHTML: true
437
437
  * // or:
438
- * // compressHTML: 'jsx'
438
+ * // compressHTML: false
439
439
  * }
440
440
  * ```
441
441
  */
@@ -4,7 +4,7 @@ import type { SSRLoadedRenderer } from '../types/public/internal.js';
4
4
  * Use this function to provide renderers to the `AstroContainer`:
5
5
  *
6
6
  * ```js
7
- * import { getContainerRenderer } from "@astrojs/react";
7
+ * import { getContainerRenderer } from "@astrojs/react/container-renderer";
8
8
  * import { experimental_AstroContainer as AstroContainer } from "astro/container";
9
9
  * import { loadRenderers } from "astro:container"; // use this only when using vite/vitest
10
10
  *
@@ -1,9 +1,9 @@
1
- import { hasHeadPropagationCall } from "../core/head-propagation/hint.js";
2
1
  import {
3
2
  buildImporterGraphFromModuleInfo,
4
3
  computeInTreeAncestors
5
4
  } from "../core/head-propagation/graph.js";
6
5
  import { getTopLevelPageModuleInfos } from "../core/build/graph.js";
6
+ import { PROPAGATED_ASSET_FLAG } from "../content/consts.js";
7
7
  import { getAstroMetadata } from "../vite-plugin-astro/index.js";
8
8
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js";
9
9
  const VIRTUAL_COMPONENT_METADATA = "virtual:astro:component-metadata";
@@ -127,12 +127,12 @@ function configHeadVitePlugin() {
127
127
  });
128
128
  }
129
129
  },
130
- transform(source, id) {
130
+ transform(_source, id) {
131
131
  let info = this.getModuleInfo(id);
132
132
  if (info && getAstroMetadata(info)?.containsHead) {
133
133
  propagateMetadata.call(this, id, "containsHead", true);
134
134
  }
135
- if (hasHeadPropagationCall(source)) {
135
+ if (id.includes(PROPAGATED_ASSET_FLAG)) {
136
136
  propagateMetadata.call(this, id, "propagation", "in-tree");
137
137
  }
138
138
  invalidateComponentMetadataModule();
@@ -140,17 +140,11 @@ function configHeadVitePlugin() {
140
140
  };
141
141
  }
142
142
  function astroHeadBuildPlugin(internals) {
143
- const headPropagationModuleIds = /* @__PURE__ */ new Set();
144
143
  return {
145
144
  name: "astro:head-metadata-build",
146
145
  applyToEnvironment(environment) {
147
146
  return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
148
147
  },
149
- transform(source, id) {
150
- if (hasHeadPropagationCall(source)) {
151
- headPropagationModuleIds.add(id);
152
- }
153
- },
154
148
  generateBundle(_opts, bundle) {
155
149
  const map = internals.componentMetadata;
156
150
  const moduleIds = /* @__PURE__ */ new Set();
@@ -167,7 +161,7 @@ function astroHeadBuildPlugin(internals) {
167
161
  }
168
162
  for (const [, output] of Object.entries(bundle)) {
169
163
  if (output.type !== "chunk") continue;
170
- for (const [id, mod] of Object.entries(output.modules)) {
164
+ for (const [id] of Object.entries(output.modules)) {
171
165
  moduleIds.add(id);
172
166
  const modinfo = this.getModuleInfo(id);
173
167
  if (modinfo) {
@@ -182,7 +176,7 @@ function astroHeadBuildPlugin(internals) {
182
176
  selfPropagationSeeds.add(id);
183
177
  }
184
178
  }
185
- if (mod.code && hasHeadPropagationCall(mod.code) || headPropagationModuleIds.has(id)) {
179
+ if (id.includes(PROPAGATED_ASSET_FLAG)) {
186
180
  commentPropagationSeeds.add(id);
187
181
  }
188
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "7.0.0-beta.4",
3
+ "version": "7.0.0-beta.5",
4
4
  "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
5
5
  "type": "module",
6
6
  "author": "withastro",
@@ -112,7 +112,7 @@
112
112
  "README.md"
113
113
  ],
114
114
  "dependencies": {
115
- "@astrojs/compiler-rs": "^0.1.10",
115
+ "@astrojs/compiler-rs": "^0.2.2",
116
116
  "@capsizecss/unpack": "^4.0.0",
117
117
  "@clack/prompts": "^1.1.0",
118
118
  "@oslojs/encoding": "^1.1.0",
@@ -166,8 +166,8 @@
166
166
  "yargs-parser": "^22.0.0",
167
167
  "zod": "^4.3.6",
168
168
  "@astrojs/internal-helpers": "0.10.0",
169
- "@astrojs/telemetry": "3.3.2",
170
- "@astrojs/markdown-satteri": "0.3.1-beta.1"
169
+ "@astrojs/markdown-satteri": "0.3.1-beta.1",
170
+ "@astrojs/telemetry": "3.3.2"
171
171
  },
172
172
  "optionalDependencies": {
173
173
  "sharp": "^0.34.0"
@@ -208,8 +208,8 @@
208
208
  "unified": "^11.0.5",
209
209
  "vitest": "^4.1.0",
210
210
  "@astrojs/check": "0.9.9",
211
- "astro-scripts": "0.0.14",
212
- "@astrojs/markdown-remark": "7.2.0"
211
+ "@astrojs/markdown-remark": "7.2.0",
212
+ "astro-scripts": "0.0.14"
213
213
  },
214
214
  "engines": {
215
215
  "node": ">=22.12.0",
@@ -1,4 +1,3 @@
1
- 'use astro:head-inject';
2
1
  import {
3
2
  createDeprecatedFunction,
4
3
  createGetCollection,
@@ -1,4 +0,0 @@
1
- /**
2
- * Returns true when source contains the `"use astro:head-inject"` directive.
3
- */
4
- export declare function hasHeadPropagationCall(source: string): boolean;
@@ -1,7 +0,0 @@
1
- const HEAD_PROPAGATION_HINT = '"use astro:head-inject"';
2
- function hasHeadPropagationCall(source) {
3
- return source.includes(HEAD_PROPAGATION_HINT);
4
- }
5
- export {
6
- hasHeadPropagationCall
7
- };