astro 6.2.2 → 6.3.0

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 (110) hide show
  1. package/dist/actions/handler.d.ts +32 -0
  2. package/dist/actions/handler.js +45 -0
  3. package/dist/actions/runtime/server.js +1 -1
  4. package/dist/assets/build/generate.js +1 -1
  5. package/dist/assets/build/remote.d.ts +3 -2
  6. package/dist/assets/build/remote.js +16 -9
  7. package/dist/assets/endpoint/generic.js +4 -7
  8. package/dist/assets/endpoint/shared.js +7 -2
  9. package/dist/assets/index.d.ts +1 -0
  10. package/dist/assets/index.js +2 -0
  11. package/dist/assets/services/sharp.js +7 -0
  12. package/dist/assets/utils/index.d.ts +1 -0
  13. package/dist/assets/utils/index.js +2 -0
  14. package/dist/assets/utils/redirectValidation.d.ts +48 -0
  15. package/dist/assets/utils/redirectValidation.js +48 -0
  16. package/dist/assets/utils/remoteProbe.js +25 -2
  17. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  18. package/dist/container/index.js +18 -14
  19. package/dist/content/content-layer.js +3 -4
  20. package/dist/content/server-listeners.js +0 -4
  21. package/dist/content/vite-plugin-content-virtual-mod.js +9 -1
  22. package/dist/core/app/base.d.ts +33 -15
  23. package/dist/core/app/base.js +120 -324
  24. package/dist/core/app/dev/app.d.ts +3 -2
  25. package/dist/core/app/dev/app.js +4 -60
  26. package/dist/core/app/entrypoints/virtual/dev.js +2 -0
  27. package/dist/core/app/entrypoints/virtual/prod.js +4 -1
  28. package/dist/core/app/prepare-response.d.ts +11 -0
  29. package/dist/core/app/prepare-response.js +18 -0
  30. package/dist/core/app/render-options.d.ts +11 -0
  31. package/dist/core/app/render-options.js +11 -0
  32. package/dist/core/base-pipeline.d.ts +38 -1
  33. package/dist/core/base-pipeline.js +50 -7
  34. package/dist/core/build/app.d.ts +3 -4
  35. package/dist/core/build/app.js +3 -17
  36. package/dist/core/cache/handler.d.ts +29 -0
  37. package/dist/core/cache/handler.js +81 -0
  38. package/dist/core/config/schemas/base.d.ts +4 -0
  39. package/dist/core/config/schemas/base.js +4 -0
  40. package/dist/core/config/schemas/relative.d.ts +6 -0
  41. package/dist/core/constants.d.ts +27 -1
  42. package/dist/core/constants.js +14 -1
  43. package/dist/core/cookies/cookies.d.ts +7 -2
  44. package/dist/core/cookies/cookies.js +11 -4
  45. package/dist/core/cookies/response.d.ts +1 -1
  46. package/dist/core/cookies/response.js +1 -2
  47. package/dist/core/create-vite.js +15 -0
  48. package/dist/core/csp/runtime.js +6 -4
  49. package/dist/core/dev/dev.js +1 -1
  50. package/dist/core/errors/build-handler.d.ts +17 -0
  51. package/dist/core/errors/build-handler.js +22 -0
  52. package/dist/core/errors/default-handler.d.ts +14 -0
  53. package/dist/core/errors/default-handler.js +144 -0
  54. package/dist/core/errors/dev-handler.d.ts +21 -0
  55. package/dist/core/errors/dev-handler.js +82 -0
  56. package/dist/core/errors/handler.d.ts +9 -0
  57. package/dist/core/errors/handler.js +0 -0
  58. package/dist/core/fetch/default-handler.d.ts +17 -0
  59. package/dist/core/fetch/default-handler.js +45 -0
  60. package/dist/core/fetch/fetch-state.d.ts +244 -0
  61. package/dist/core/fetch/fetch-state.js +779 -0
  62. package/dist/core/fetch/index.d.ts +61 -0
  63. package/dist/core/fetch/index.js +121 -0
  64. package/dist/core/fetch/types.d.ts +6 -0
  65. package/dist/core/fetch/types.js +0 -0
  66. package/dist/core/fetch/vite-plugin.d.ts +5 -0
  67. package/dist/core/fetch/vite-plugin.js +69 -0
  68. package/dist/core/hono/index.d.ts +21 -0
  69. package/dist/core/hono/index.js +98 -0
  70. package/dist/core/i18n/handler.d.ts +18 -0
  71. package/dist/core/i18n/handler.js +119 -0
  72. package/dist/core/logger/core.d.ts +8 -0
  73. package/dist/core/logger/core.js +16 -0
  74. package/dist/core/messages/runtime.js +1 -1
  75. package/dist/core/middleware/astro-middleware.d.ts +27 -0
  76. package/dist/core/middleware/astro-middleware.js +53 -0
  77. package/dist/core/pages/handler.d.ts +20 -0
  78. package/dist/core/pages/handler.js +74 -0
  79. package/dist/core/redirects/render.d.ts +2 -2
  80. package/dist/core/redirects/render.js +7 -8
  81. package/dist/core/rewrites/handler.d.ts +37 -0
  82. package/dist/core/rewrites/handler.js +67 -0
  83. package/dist/core/routing/3xx.js +8 -4
  84. package/dist/core/routing/handler.d.ts +17 -0
  85. package/dist/core/routing/handler.js +172 -0
  86. package/dist/core/routing/match.d.ts +0 -7
  87. package/dist/core/routing/match.js +0 -5
  88. package/dist/core/routing/trailing-slash-handler.d.ts +18 -0
  89. package/dist/core/routing/trailing-slash-handler.js +67 -0
  90. package/dist/core/session/drivers.d.ts +1 -1
  91. package/dist/core/session/handler.d.ts +11 -0
  92. package/dist/core/session/handler.js +33 -0
  93. package/dist/core/util/normalized-url.d.ts +10 -0
  94. package/dist/core/util/normalized-url.js +21 -0
  95. package/dist/i18n/middleware.d.ts +10 -0
  96. package/dist/i18n/middleware.js +4 -88
  97. package/dist/i18n/utils.js +2 -2
  98. package/dist/runtime/server/astro-island.js +57 -20
  99. package/dist/runtime/server/astro-island.prebuilt-dev.d.ts +1 -1
  100. package/dist/runtime/server/astro-island.prebuilt-dev.js +1 -1
  101. package/dist/runtime/server/astro-island.prebuilt.d.ts +1 -1
  102. package/dist/runtime/server/astro-island.prebuilt.js +1 -1
  103. package/dist/runtime/server/render/server-islands.js +2 -1
  104. package/dist/types/public/config.d.ts +46 -12
  105. package/dist/types/public/internal.d.ts +1 -1
  106. package/dist/vite-plugin-app/app.d.ts +4 -5
  107. package/dist/vite-plugin-app/app.js +20 -65
  108. package/package.json +11 -5
  109. package/dist/core/render-context.d.ts +0 -77
  110. package/dist/core/render-context.js +0 -826
@@ -1,9 +1,10 @@
1
1
  import type { RoutesList } from '../../types/astro.js';
2
2
  import type { RemotePattern, RouteData } from '../../types/public/index.js';
3
- import type { Pipeline } from '../base-pipeline.js';
3
+ import { type Pipeline } from '../base-pipeline.js';
4
4
  import { getSetCookiesFromResponse } from '../cookies/index.js';
5
5
  import { AstroIntegrationLogger, type AstroLogger } from '../logger/core.js';
6
- import { type CreateRenderContext, RenderContext } from '../render-context.js';
6
+ import type { FetchHandler } from '../fetch/types.js';
7
+ import type { ErrorHandler } from '../errors/handler.js';
7
8
  import type { WaitUntilHook } from '../wait-until.js';
8
9
  import type { AppPipeline } from './pipeline.js';
9
10
  import type { SSRManifest } from './types.js';
@@ -60,7 +61,7 @@ export interface RenderOptions {
60
61
  routeData?: RouteData;
61
62
  }
62
63
  type RequiredRenderOptions = Required<RenderOptions>;
63
- interface ResolvedRenderOptions {
64
+ export interface ResolvedRenderOptions {
64
65
  addCookieHeader: RequiredRenderOptions['addCookieHeader'];
65
66
  clientAddress: RequiredRenderOptions['clientAddress'] | undefined;
66
67
  prerenderedErrorPageFetch: RequiredRenderOptions['prerenderedErrorPageFetch'] | undefined;
@@ -79,20 +80,38 @@ export interface RenderErrorOptions extends ResolvedRenderOptions {
79
80
  * Allows passing an error to 500.astro. It will be available through `Astro.props.error`.
80
81
  */
81
82
  error?: unknown;
83
+ /**
84
+ * The pathname to use for the error page render context. If omitted, the
85
+ * error handler computes it from `request` via a short-lived `FetchState`.
86
+ */
87
+ pathname?: string;
82
88
  }
83
89
  type ErrorPagePath = `${string}/404` | `${string}/500` | `${string}/404/` | `${string}/500/` | `${string}404.html` | `${string}500.html`;
84
90
  export declare abstract class BaseApp<P extends Pipeline = AppPipeline> {
85
91
  #private;
86
92
  manifest: SSRManifest;
87
- manifestData: RoutesList;
93
+ manifestData: {
94
+ routes: RouteData[];
95
+ };
88
96
  pipeline: P;
89
97
  baseWithoutTrailingSlash: string;
90
98
  get logger(): AstroLogger;
91
99
  get adapterLogger(): AstroIntegrationLogger;
92
100
  constructor(manifest: SSRManifest, streaming?: boolean, ...args: any[]);
101
+ /**
102
+ * Override the fetch handler used to dispatch requests. Entrypoints
103
+ * call this with the default export of `virtual:astro:fetchable` to
104
+ * plug in a user-authored handler from `src/app.ts`.
105
+ */
106
+ setFetchHandler(handler: {
107
+ fetch: FetchHandler;
108
+ }): void;
109
+ /**
110
+ * Returns the error handler strategy used by this app. Override to
111
+ * provide environment-specific behavior (dev overlay, build-time throws, etc.).
112
+ */
113
+ protected createErrorHandler(): ErrorHandler;
93
114
  abstract isDev(): boolean;
94
- createRenderContext(payload: CreateRenderContext): Promise<RenderContext>;
95
- getAdapterLogger(): AstroIntegrationLogger;
96
115
  /**
97
116
  * Resets the cached adapter logger so it picks up a new logger instance.
98
117
  * Used by BuildApp when the logger is replaced via setOptions().
@@ -113,10 +132,8 @@ export declare abstract class BaseApp<P extends Pipeline = AppPipeline> {
113
132
  set setManifestData(newManifestData: RoutesList);
114
133
  removeBase(pathname: string): string;
115
134
  /**
116
- * It removes the base from the request URL, prepends it with a forward slash and attempts to decoded it.
117
- *
118
- * If the decoding fails, it logs the error and return the pathname as is.
119
- * @param request
135
+ * Extracts the base-stripped, decoded pathname from a request.
136
+ * Used by adapters to compute the pathname for dev-mode route matching.
120
137
  */
121
138
  getPathnameFromRequest(request: Request): string;
122
139
  /**
@@ -128,7 +145,6 @@ export declare abstract class BaseApp<P extends Pipeline = AppPipeline> {
128
145
  * @param allowPrerenderedRoutes
129
146
  */
130
147
  match(request: Request, allowPrerenderedRoutes?: boolean): RouteData | undefined;
131
- private createRouter;
132
148
  /**
133
149
  * A matching route function to use in the development server.
134
150
  * Contrary to the `.match` function, this function resolves props and params, returning the correct
@@ -137,7 +153,6 @@ export declare abstract class BaseApp<P extends Pipeline = AppPipeline> {
137
153
  */
138
154
  devMatch(pathname?: string): Promise<DevMatch | undefined> | undefined;
139
155
  private computePathnameFromDomain;
140
- private redirectTrailingSlash;
141
156
  render(request: Request, { addCookieHeader, clientAddress, locals, prerenderedErrorPageFetch, routeData, waitUntil, }?: RenderOptions): Promise<Response>;
142
157
  setCookieHeaders(response: Response): Generator<string, string[], any>;
143
158
  /**
@@ -154,10 +169,13 @@ export declare abstract class BaseApp<P extends Pipeline = AppPipeline> {
154
169
  static getSetCookieFromResponse: typeof getSetCookiesFromResponse;
155
170
  /**
156
171
  * If it is a known error code, try sending the according page (e.g. 404.astro / 500.astro).
157
- * This also handles pre-rendered /404 or /500 routes
172
+ * This also handles pre-rendered /404 or /500 routes.
173
+ *
174
+ * Delegates to the app's configured `ErrorHandler`. To customize behavior
175
+ * for a specific environment, override `createErrorHandler()` rather than
176
+ * this method.
158
177
  */
159
- renderError(request: Request, { status, response: originalResponse, skipMiddleware, error, ...resolvedRenderOptions }: RenderErrorOptions): Promise<Response>;
160
- private mergeResponses;
178
+ renderError(request: Request, options: RenderErrorOptions): Promise<Response>;
161
179
  getDefaultStatusCode(routeData: RouteData, pathname: string): number;
162
180
  getManifest(): SSRManifest;
163
181
  logThisRequest({ pathname, method, statusCode, isRewrite, timeStart, }: {
@@ -1,48 +1,46 @@
1
1
  import {
2
2
  appendForwardSlash,
3
3
  collapseDuplicateLeadingSlashes,
4
- collapseDuplicateTrailingSlashes,
5
- hasFileExtension,
6
- isInternalPath,
7
4
  joinPaths,
8
5
  prependForwardSlash,
9
6
  removeTrailingForwardSlash
10
7
  } from "@astrojs/internal-helpers/path";
11
8
  import { matchPattern } from "@astrojs/internal-helpers/remote";
12
9
  import { normalizeTheLocale } from "../../i18n/index.js";
13
- import {
14
- clientAddressSymbol,
15
- DEFAULT_404_COMPONENT,
16
- NOOP_MIDDLEWARE_HEADER,
17
- REROUTABLE_STATUS_CODES,
18
- REROUTE_DIRECTIVE_HEADER,
19
- responseSentSymbol,
20
- REWRITE_DIRECTIVE_HEADER_KEY,
21
- ROUTE_TYPE_HEADER
22
- } from "../constants.js";
23
- import {
24
- AstroCookies,
25
- attachCookiesToResponse,
26
- getSetCookiesFromResponse
27
- } from "../cookies/index.js";
28
- import { getCookiesFromResponse } from "../cookies/response.js";
10
+ import { PipelineFeatures } from "../base-pipeline.js";
11
+ import { ASTRO_ERROR_HEADER, clientAddressSymbol } from "../constants.js";
12
+ import { getSetCookiesFromResponse } from "../cookies/index.js";
29
13
  import { AstroError, AstroErrorData } from "../errors/index.js";
30
14
  import { AstroIntegrationLogger } from "../logger/core.js";
31
- import { RenderContext } from "../render-context.js";
32
- import { redirectTemplate } from "../routing/3xx.js";
33
- import { ensure404Route } from "../routing/astro-designed-error-pages.js";
34
- import { routeHasHtmlExtension } from "../routing/helpers.js";
35
- import { matchRoute } from "../routing/match.js";
36
- import { applyCacheHeaders } from "../cache/runtime/cache.js";
37
- import { Router } from "../routing/router.js";
38
- import { PERSIST_SYMBOL } from "../session/runtime.js";
15
+ import { DefaultFetchHandler } from "../fetch/default-handler.js";
16
+ import { appSymbol } from "../constants.js";
17
+ import { DefaultErrorHandler } from "../errors/default-handler.js";
18
+ import { setRenderOptions } from "./render-options.js";
39
19
  class BaseApp {
40
20
  manifest;
41
21
  manifestData;
42
22
  pipeline;
43
23
  #adapterLogger;
44
24
  baseWithoutTrailingSlash;
45
- #router;
25
+ /**
26
+ * The handler that turns incoming `Request` objects into `Response`s.
27
+ * Defaults to a `DefaultFetchHandler` pinned to this app and can be
28
+ * overridden via `setFetchHandler` — typically by the bundled
29
+ * entrypoint after importing `virtual:astro:fetchable`.
30
+ */
31
+ #fetchHandler;
32
+ #errorHandler;
33
+ /**
34
+ * Whether a custom fetch handler (from `src/app.ts`) has been set
35
+ * via `setFetchHandler`. When false, the `DefaultFetchHandler` is
36
+ * in use and all features are implicitly active.
37
+ */
38
+ #hasCustomFetchHandler = false;
39
+ /**
40
+ * Whether the missing-feature check has already run. We only want
41
+ * to warn once — after the first request in dev, or at build end.
42
+ */
43
+ #featureCheckDone = false;
46
44
  get logger() {
47
45
  return this.pipeline.logger;
48
46
  }
@@ -57,17 +55,27 @@ class BaseApp {
57
55
  }
58
56
  constructor(manifest, streaming = true, ...args) {
59
57
  this.manifest = manifest;
60
- this.manifestData = { routes: manifest.routes.map((route) => route.routeData) };
61
58
  this.baseWithoutTrailingSlash = removeTrailingForwardSlash(manifest.base);
62
59
  this.pipeline = this.createPipeline(streaming, manifest, ...args);
63
- ensure404Route(this.manifestData);
64
- this.#router = this.createRouter(this.manifestData);
60
+ this.manifestData = this.pipeline.manifestData;
61
+ this.#fetchHandler = new DefaultFetchHandler(this);
62
+ this.#errorHandler = this.createErrorHandler();
65
63
  }
66
- async createRenderContext(payload) {
67
- return RenderContext.create(payload);
64
+ /**
65
+ * Override the fetch handler used to dispatch requests. Entrypoints
66
+ * call this with the default export of `virtual:astro:fetchable` to
67
+ * plug in a user-authored handler from `src/app.ts`.
68
+ */
69
+ setFetchHandler(handler) {
70
+ this.#fetchHandler = handler;
71
+ this.#hasCustomFetchHandler = !(handler instanceof DefaultFetchHandler);
68
72
  }
69
- getAdapterLogger() {
70
- return this.adapterLogger;
73
+ /**
74
+ * Returns the error handler strategy used by this app. Override to
75
+ * provide environment-specific behavior (dev overlay, build-time throws, etc.).
76
+ */
77
+ createErrorHandler() {
78
+ return new DefaultErrorHandler(this);
71
79
  }
72
80
  /**
73
81
  * Resets the cached adapter logger so it picks up a new logger instance.
@@ -97,7 +105,8 @@ class BaseApp {
97
105
  }
98
106
  set setManifestData(newManifestData) {
99
107
  this.manifestData = newManifestData;
100
- this.#router = this.createRouter(this.manifestData);
108
+ this.pipeline.manifestData = newManifestData;
109
+ this.pipeline.rebuildRouter();
101
110
  }
102
111
  removeBase(pathname) {
103
112
  pathname = collapseDuplicateLeadingSlashes(pathname);
@@ -107,10 +116,8 @@ class BaseApp {
107
116
  return pathname;
108
117
  }
109
118
  /**
110
- * It removes the base from the request URL, prepends it with a forward slash and attempts to decoded it.
111
- *
112
- * If the decoding fails, it logs the error and return the pathname as is.
113
- * @param request
119
+ * Extracts the base-stripped, decoded pathname from a request.
120
+ * Used by adapters to compute the pathname for dev-mode route matching.
114
121
  */
115
122
  getPathnameFromRequest(request) {
116
123
  const url = new URL(request.url);
@@ -118,7 +125,7 @@ class BaseApp {
118
125
  try {
119
126
  return decodeURI(pathname);
120
127
  } catch (e) {
121
- this.getAdapterLogger().error(e.toString());
128
+ this.adapterLogger.error(e.toString());
122
129
  return pathname;
123
130
  }
124
131
  }
@@ -137,23 +144,16 @@ class BaseApp {
137
144
  if (!pathname) {
138
145
  pathname = prependForwardSlash(this.removeBase(url.pathname));
139
146
  }
140
- const match = this.#router.match(decodeURI(pathname), { allowWithoutBase: true });
141
- if (match.type !== "match") return void 0;
142
- const routeData = match.route;
147
+ const routeData = this.pipeline.matchRoute(decodeURI(pathname));
148
+ if (!routeData) return void 0;
143
149
  if (allowPrerenderedRoutes) {
144
150
  return routeData;
145
- } else if (routeData.prerender) {
151
+ }
152
+ if (routeData.prerender) {
146
153
  return void 0;
147
154
  }
148
155
  return routeData;
149
156
  }
150
- createRouter(manifestData) {
151
- return new Router(manifestData.routes, {
152
- base: this.manifest.base,
153
- trailingSlash: this.manifest.trailingSlash,
154
- buildFormat: this.manifest.buildFormat
155
- });
156
- }
157
157
  /**
158
158
  * A matching route function to use in the development server.
159
159
  * Contrary to the `.match` function, this function resolves props and params, returning the correct
@@ -215,26 +215,6 @@ class BaseApp {
215
215
  }
216
216
  return pathname;
217
217
  }
218
- redirectTrailingSlash(pathname) {
219
- const { trailingSlash } = this.manifest;
220
- if (pathname === "/" || isInternalPath(pathname)) {
221
- return pathname;
222
- }
223
- const path = collapseDuplicateTrailingSlashes(pathname, trailingSlash !== "never");
224
- if (path !== pathname) {
225
- return path;
226
- }
227
- if (trailingSlash === "ignore") {
228
- return pathname;
229
- }
230
- if (trailingSlash === "always" && !hasFileExtension(pathname)) {
231
- return appendForwardSlash(pathname);
232
- }
233
- if (trailingSlash === "never") {
234
- return removeTrailingForwardSlash(pathname);
235
- }
236
- return pathname;
237
- }
238
218
  async render(request, {
239
219
  addCookieHeader = false,
240
220
  clientAddress = Reflect.get(request, clientAddressSymbol),
@@ -244,28 +224,6 @@ class BaseApp {
244
224
  waitUntil
245
225
  } = {}) {
246
226
  await this.pipeline.getLogger();
247
- const timeStart = performance.now();
248
- const url = new URL(request.url);
249
- const redirect = this.redirectTrailingSlash(url.pathname);
250
- if (redirect !== url.pathname) {
251
- const status = request.method === "GET" ? 301 : 308;
252
- const response2 = new Response(
253
- redirectTemplate({
254
- status,
255
- relativeLocation: url.pathname,
256
- absoluteLocation: redirect,
257
- from: request.url
258
- }),
259
- {
260
- status,
261
- headers: {
262
- location: redirect + url.search
263
- }
264
- }
265
- );
266
- this.#prepareResponse(response2, { addCookieHeader });
267
- return response2;
268
- }
269
227
  if (routeData) {
270
228
  this.logger.debug(
271
229
  "router",
@@ -275,150 +233,64 @@ class BaseApp {
275
233
  this.logger.debug("router", "RouteData");
276
234
  this.logger.debug("router", routeData);
277
235
  }
278
- const resolvedRenderOptions = {
279
- addCookieHeader,
280
- clientAddress,
281
- prerenderedErrorPageFetch,
282
- locals,
283
- routeData,
284
- waitUntil
285
- };
286
236
  if (locals) {
287
237
  if (typeof locals !== "object") {
288
238
  const error = new AstroError(AstroErrorData.LocalsNotAnObject);
289
239
  this.logger.error(null, error.stack);
290
240
  return this.renderError(request, {
291
- ...resolvedRenderOptions,
241
+ addCookieHeader,
242
+ clientAddress,
243
+ prerenderedErrorPageFetch,
292
244
  // If locals are invalid, we don't want to include them when
293
245
  // rendering the error page
294
246
  locals: void 0,
247
+ routeData,
248
+ waitUntil,
295
249
  status: 500,
296
250
  error
297
251
  });
298
252
  }
299
253
  }
300
254
  if (!routeData) {
301
- if (this.isDev()) {
302
- const result = await this.devMatch(this.getPathnameFromRequest(request));
303
- if (result) {
304
- routeData = result.routeData;
305
- }
306
- } else {
307
- routeData = this.match(request);
255
+ const domainPathname = this.computePathnameFromDomain(request);
256
+ if (domainPathname) {
257
+ routeData = this.pipeline.matchRoute(decodeURI(domainPathname));
308
258
  }
309
- this.logger.debug("router", "Astro matched the following route for " + request.url);
310
- this.logger.debug("router", "RouteData:\n" + routeData);
311
- }
312
- if (!routeData) {
313
- routeData = this.manifestData.routes.find(
314
- (route) => route.component === "404.astro" || route.component === DEFAULT_404_COMPONENT
315
- );
316
259
  }
317
- if (!routeData) {
318
- this.logger.debug("router", "Astro hasn't found routes that match " + request.url);
319
- this.logger.debug("router", "Here's the available routes:\n", this.manifestData);
320
- return this.renderError(request, {
321
- ...resolvedRenderOptions,
322
- status: 404
323
- });
324
- }
325
- let pathname = this.getPathnameFromRequest(request);
326
- if (this.isDev() && !routeHasHtmlExtension(routeData)) {
327
- pathname = pathname.replace(/\/index\.html$/, "/").replace(/\.html$/, "");
328
- }
329
- const defaultStatus = this.getDefaultStatusCode(routeData, pathname);
260
+ const resolvedOptions = {
261
+ addCookieHeader,
262
+ clientAddress,
263
+ prerenderedErrorPageFetch,
264
+ locals,
265
+ routeData,
266
+ waitUntil
267
+ };
330
268
  let response;
331
- let session;
332
- let cache;
333
- try {
334
- const componentInstance = await this.pipeline.getComponentByRoute(routeData);
335
- const renderContext = await this.createRenderContext({
336
- pipeline: this.pipeline,
269
+ if (this.#fetchHandler instanceof DefaultFetchHandler) {
270
+ Reflect.set(request, appSymbol, this);
271
+ response = await this.#fetchHandler.renderWithOptions(request, resolvedOptions);
272
+ } else {
273
+ setRenderOptions(request, resolvedOptions);
274
+ Reflect.set(request, appSymbol, this);
275
+ response = await this.#fetchHandler.fetch(request);
276
+ }
277
+ this.#warnMissingFeatures();
278
+ if (response.headers.get(ASTRO_ERROR_HEADER)) {
279
+ response.headers.delete(ASTRO_ERROR_HEADER);
280
+ return this.renderError(request, {
281
+ addCookieHeader,
282
+ clientAddress,
283
+ prerenderedErrorPageFetch,
337
284
  locals,
338
- pathname,
339
- request,
340
285
  routeData,
341
- status: defaultStatus,
342
- clientAddress
343
- });
344
- session = renderContext.session;
345
- cache = renderContext.cache;
346
- if (this.pipeline.cacheProvider) {
347
- const cacheProvider = await this.pipeline.getCacheProvider();
348
- if (cacheProvider?.onRequest) {
349
- response = await cacheProvider.onRequest(
350
- {
351
- request,
352
- url: new URL(request.url),
353
- waitUntil: resolvedRenderOptions.waitUntil
354
- },
355
- async () => {
356
- const res = await renderContext.render(componentInstance);
357
- applyCacheHeaders(cache, res);
358
- return res;
359
- }
360
- );
361
- response.headers.delete("CDN-Cache-Control");
362
- response.headers.delete("Cache-Tag");
363
- } else {
364
- response = await renderContext.render(componentInstance);
365
- applyCacheHeaders(cache, response);
366
- }
367
- } else {
368
- response = await renderContext.render(componentInstance);
369
- }
370
- const isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY);
371
- this.logThisRequest({
372
- pathname,
373
- method: request.method,
374
- statusCode: response.status,
375
- isRewrite,
376
- timeStart
377
- });
378
- } catch (err) {
379
- this.logger.error(null, err.stack || err.message || String(err));
380
- return this.renderError(request, {
381
- ...resolvedRenderOptions,
382
- status: 500,
383
- error: err
384
- });
385
- } finally {
386
- await session?.[PERSIST_SYMBOL]();
387
- }
388
- if (REROUTABLE_STATUS_CODES.includes(response.status) && // If the body isn't null, that means the user sets the 404 status
389
- // but uses the current route to handle the 404
390
- response.body === null && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
391
- return this.renderError(request, {
392
- ...resolvedRenderOptions,
286
+ waitUntil,
393
287
  response,
394
288
  status: response.status,
395
- // We don't have an error to report here. Passing null means we pass nothing intentionally
396
- // while undefined means there's no error
397
289
  error: response.status === 500 ? null : void 0
398
290
  });
399
291
  }
400
- this.#prepareResponse(response, { addCookieHeader });
401
292
  return response;
402
293
  }
403
- #prepareResponse(response, { addCookieHeader }) {
404
- for (const headerName of [
405
- REROUTE_DIRECTIVE_HEADER,
406
- REWRITE_DIRECTIVE_HEADER_KEY,
407
- NOOP_MIDDLEWARE_HEADER,
408
- ROUTE_TYPE_HEADER
409
- ]) {
410
- if (response.headers.has(headerName)) {
411
- response.headers.delete(headerName);
412
- }
413
- }
414
- if (addCookieHeader) {
415
- for (const setCookieHeaderValue of getSetCookiesFromResponse(response)) {
416
- response.headers.append("set-cookie", setCookieHeaderValue);
417
- }
418
- }
419
- this.logger.flush();
420
- Reflect.set(response, responseSentSymbol, true);
421
- }
422
294
  setCookieHeaders(response) {
423
295
  return getSetCookiesFromResponse(response);
424
296
  }
@@ -436,126 +308,50 @@ class BaseApp {
436
308
  static getSetCookieFromResponse = getSetCookiesFromResponse;
437
309
  /**
438
310
  * If it is a known error code, try sending the according page (e.g. 404.astro / 500.astro).
439
- * This also handles pre-rendered /404 or /500 routes
311
+ * This also handles pre-rendered /404 or /500 routes.
312
+ *
313
+ * Delegates to the app's configured `ErrorHandler`. To customize behavior
314
+ * for a specific environment, override `createErrorHandler()` rather than
315
+ * this method.
440
316
  */
441
- async renderError(request, {
442
- status,
443
- response: originalResponse,
444
- skipMiddleware = false,
445
- error,
446
- ...resolvedRenderOptions
447
- }) {
448
- const errorRoutePath = `/${status}${this.manifest.trailingSlash === "always" ? "/" : ""}`;
449
- const errorRouteData = matchRoute(errorRoutePath, this.manifestData);
450
- const url = new URL(request.url);
451
- if (errorRouteData) {
452
- if (errorRouteData.prerender) {
453
- const maybeDotHtml = errorRouteData.route.endsWith(`/${status}`) ? ".html" : "";
454
- const statusURL = new URL(`${this.baseWithoutTrailingSlash}/${status}${maybeDotHtml}`, url);
455
- if (statusURL.toString() !== request.url && resolvedRenderOptions.prerenderedErrorPageFetch) {
456
- const response2 = await resolvedRenderOptions.prerenderedErrorPageFetch(
457
- statusURL.toString()
458
- );
459
- const override = { status, removeContentEncodingHeaders: true };
460
- const newResponse = this.mergeResponses(response2, originalResponse, override);
461
- this.#prepareResponse(newResponse, resolvedRenderOptions);
462
- return newResponse;
463
- }
464
- }
465
- const mod = await this.pipeline.getComponentByRoute(errorRouteData);
466
- let session;
467
- try {
468
- const renderContext = await this.createRenderContext({
469
- locals: resolvedRenderOptions.locals,
470
- pipeline: this.pipeline,
471
- skipMiddleware,
472
- pathname: this.getPathnameFromRequest(request),
473
- request,
474
- routeData: errorRouteData,
475
- status,
476
- props: { error },
477
- clientAddress: resolvedRenderOptions.clientAddress
478
- });
479
- session = renderContext.session;
480
- const response2 = await renderContext.render(mod);
481
- const newResponse = this.mergeResponses(response2, originalResponse);
482
- this.#prepareResponse(newResponse, resolvedRenderOptions);
483
- return newResponse;
484
- } catch {
485
- if (skipMiddleware === false) {
486
- return this.renderError(request, {
487
- ...resolvedRenderOptions,
488
- status,
489
- response: originalResponse,
490
- skipMiddleware: true
491
- });
492
- }
493
- } finally {
494
- await session?.[PERSIST_SYMBOL]();
495
- }
496
- }
497
- const response = this.mergeResponses(new Response(null, { status }), originalResponse);
498
- this.#prepareResponse(response, resolvedRenderOptions);
499
- return response;
317
+ async renderError(request, options) {
318
+ return this.#errorHandler.renderError(request, options);
500
319
  }
501
- mergeResponses(newResponse, originalResponse, override) {
502
- let newResponseHeaders = newResponse.headers;
503
- if (override?.removeContentEncodingHeaders) {
504
- newResponseHeaders = new Headers(newResponseHeaders);
505
- newResponseHeaders.delete("Content-Encoding");
506
- newResponseHeaders.delete("Content-Length");
320
+ /**
321
+ * One-shot check: after the first request with a custom `src/app.ts`,
322
+ * compare `usedFeatures` against the manifest and warn about any
323
+ * configured features the user's pipeline doesn't call.
324
+ */
325
+ #warnMissingFeatures() {
326
+ if (this.#featureCheckDone || !this.#hasCustomFetchHandler) return;
327
+ this.#featureCheckDone = true;
328
+ const manifest = this.manifest;
329
+ const missing = [];
330
+ const used = this.pipeline.usedFeatures;
331
+ if (manifest.routes.some((r) => r.routeData.type === "redirect") && !(used & PipelineFeatures.redirects)) {
332
+ missing.push("redirects");
507
333
  }
508
- if (!originalResponse) {
509
- if (override !== void 0) {
510
- return new Response(newResponse.body, {
511
- status: override.status,
512
- statusText: newResponse.statusText,
513
- headers: newResponseHeaders
514
- });
515
- }
516
- return newResponse;
334
+ if (manifest.sessionConfig && !(used & PipelineFeatures.sessions)) {
335
+ missing.push("sessions");
517
336
  }
518
- const status = override?.status ? override.status : originalResponse.status === 200 ? newResponse.status : originalResponse.status;
519
- try {
520
- originalResponse.headers.delete("Content-type");
521
- originalResponse.headers.delete("Content-Length");
522
- originalResponse.headers.delete("Transfer-Encoding");
523
- } catch {
337
+ if (manifest.actions && !(used & PipelineFeatures.actions)) {
338
+ missing.push("actions");
524
339
  }
525
- const newHeaders = new Headers();
526
- const seen = /* @__PURE__ */ new Set();
527
- for (const [name, value] of originalResponse.headers) {
528
- newHeaders.append(name, value);
529
- seen.add(name.toLowerCase());
340
+ if (manifest.middleware && !(used & PipelineFeatures.middleware)) {
341
+ missing.push("middleware");
530
342
  }
531
- for (const [name, value] of newResponseHeaders) {
532
- if (!seen.has(name.toLowerCase())) {
533
- newHeaders.append(name, value);
534
- }
343
+ if (manifest.i18n && manifest.i18n.strategy !== "manual" && !(used & PipelineFeatures.i18n)) {
344
+ missing.push("i18n");
535
345
  }
536
- const mergedResponse = new Response(newResponse.body, {
537
- status,
538
- statusText: status === 200 ? newResponse.statusText : originalResponse.statusText,
539
- // If you're looking at here for possible bugs, it means that it's not a bug.
540
- // With the middleware, users can meddle with headers, and we should pass to the 404/500.
541
- // If users see something weird, it's because they are setting some headers they should not.
542
- //
543
- // Although, we don't want it to replace the content-type, because the error page must return `text/html`
544
- headers: newHeaders
545
- });
546
- const originalCookies = getCookiesFromResponse(originalResponse);
547
- const newCookies = getCookiesFromResponse(newResponse);
548
- if (originalCookies) {
549
- if (newCookies) {
550
- for (const cookieValue of AstroCookies.consume(newCookies)) {
551
- originalResponse.headers.append("set-cookie", cookieValue);
552
- }
553
- }
554
- attachCookiesToResponse(mergedResponse, originalCookies);
555
- } else if (newCookies) {
556
- attachCookiesToResponse(mergedResponse, newCookies);
346
+ if (manifest.cacheConfig && !(used & PipelineFeatures.cache)) {
347
+ missing.push("cache");
348
+ }
349
+ for (const feature of missing) {
350
+ this.logger.warn(
351
+ "router",
352
+ `Your project uses ${feature}, but your custom src/app.ts does not call the ${feature}() handler. This feature will not work unless you add it to your app.ts pipeline.`
353
+ );
557
354
  }
558
- return mergedResponse;
559
355
  }
560
356
  getDefaultStatusCode(routeData, pathname) {
561
357
  if (!routeData.pattern.test(pathname)) {