summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/brown-pets-clean.md7
-rw-r--r--packages/astro/src/core/app/index.ts14
-rw-r--r--packages/astro/src/core/constants.ts2
-rw-r--r--packages/astro/src/core/render/params-and-props.ts3
-rw-r--r--packages/astro/src/core/routing/astro-designed-error-pages.ts20
-rw-r--r--packages/astro/src/runtime/server/render/astro/factory.ts2
-rw-r--r--packages/astro/src/runtime/server/render/astro/instance.ts2
-rw-r--r--packages/astro/src/vite-plugin-astro-server/pipeline.ts6
-rw-r--r--packages/astro/src/vite-plugin-astro-server/plugin.ts7
-rw-r--r--packages/astro/src/vite-plugin-astro-server/response.ts13
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts17
-rw-r--r--packages/astro/test/astro-dev-headers.test.js4
-rw-r--r--packages/astro/test/fixtures/virtual-routes/astro.config.js6
-rw-r--r--packages/astro/test/fixtures/virtual-routes/package.json7
-rw-r--r--packages/astro/test/fixtures/virtual-routes/src/middleware.js8
-rw-r--r--packages/astro/test/units/routing/trailing-slash.test.js3
-rw-r--r--packages/astro/test/virtual-routes.test.js30
-rw-r--r--pnpm-lock.yaml6
18 files changed, 142 insertions, 15 deletions
diff --git a/.changeset/brown-pets-clean.md b/.changeset/brown-pets-clean.md
new file mode 100644
index 000000000..28cc4e98f
--- /dev/null
+++ b/.changeset/brown-pets-clean.md
@@ -0,0 +1,7 @@
+---
+"astro": minor
+---
+
+Allows middleware to run when a matching page or endpoint is not found. Previously, a `pages/404.astro` or `pages/[...catch-all].astro` route had to match to allow middleware. This is now not necessary.
+
+When a route does not match in SSR deployments, your adapter may show a platform-specific 404 page instead of running Astro's SSR code. In these cases, you may still need to add a `404.astro` or fallback route with spread params, or use a routing configuration option if your adapter provides one.
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 7c8d0067a..bf0c61232 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -1,7 +1,8 @@
-import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
import { normalizeTheLocale } from '../../i18n/index.js';
+import type { ComponentInstance, ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import {
+ DEFAULT_404_COMPONENT,
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
clientAddressSymbol,
@@ -24,6 +25,7 @@ import { RenderContext } from '../render-context.js';
import { createAssetLink } from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
import { AppPipeline } from './pipeline.js';
+import { ensure404Route } from '../routing/astro-designed-error-pages.js';
export { deserializeManifest } from './common.js';
export interface RenderOptions {
@@ -82,9 +84,9 @@ export class App {
constructor(manifest: SSRManifest, streaming = true) {
this.#manifest = manifest;
- this.#manifestData = {
+ this.#manifestData = ensure404Route({
routes: manifest.routes.map((route) => route.routeData),
- };
+ });
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
this.#pipeline = this.#createPipeline(streaming);
this.#adapterLogger = new AstroIntegrationLogger(
@@ -475,6 +477,12 @@ export class App {
}
async #getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
+ if (route.component === DEFAULT_404_COMPONENT) {
+ return {
+ page: async () => ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
+ renderers: []
+ }
+ }
if (route.type === 'redirect') {
return RedirectSinglePageBuiltModule;
} else {
diff --git a/packages/astro/src/core/constants.ts b/packages/astro/src/core/constants.ts
index 1466ab86a..aabdcbcab 100644
--- a/packages/astro/src/core/constants.ts
+++ b/packages/astro/src/core/constants.ts
@@ -4,6 +4,8 @@ export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
export const REROUTE_DIRECTIVE_HEADER = 'X-Astro-Reroute';
export const ROUTE_TYPE_HEADER = 'X-Astro-Route-Type';
+export const DEFAULT_404_COMPONENT = 'astro-default-404';
+
/**
* A response with one of these status codes will be rewritten
* with the result of rendering the respective error page.
diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts
index b0a589ab1..eeae1a9b4 100644
--- a/packages/astro/src/core/render/params-and-props.ts
+++ b/packages/astro/src/core/render/params-and-props.ts
@@ -1,4 +1,5 @@
import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro.js';
+import { DEFAULT_404_COMPONENT } from '../constants.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Logger } from '../logger/core.js';
import { routeIsFallback } from '../redirects/helpers.js';
@@ -24,7 +25,7 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> {
return {};
}
- if (routeIsRedirect(route) || routeIsFallback(route)) {
+ if (routeIsRedirect(route) || routeIsFallback(route) || route.component === DEFAULT_404_COMPONENT) {
return {};
}
diff --git a/packages/astro/src/core/routing/astro-designed-error-pages.ts b/packages/astro/src/core/routing/astro-designed-error-pages.ts
new file mode 100644
index 000000000..ac2b08274
--- /dev/null
+++ b/packages/astro/src/core/routing/astro-designed-error-pages.ts
@@ -0,0 +1,20 @@
+import type { ManifestData } from "../../@types/astro.js";
+import { DEFAULT_404_COMPONENT } from "../constants.js";
+
+export function ensure404Route(manifest: ManifestData) {
+ if (!manifest.routes.some(route => route.route === '/404')) {
+ manifest.routes.push({
+ component: DEFAULT_404_COMPONENT,
+ generate: () => '',
+ params: [],
+ pattern: /\/404/,
+ prerender: false,
+ segments: [],
+ type: 'page',
+ route: '/404',
+ fallbackRoutes: [],
+ isIndex: false,
+ })
+ }
+ return manifest;
+}
diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts
index 68d5af739..f298a4ac1 100644
--- a/packages/astro/src/runtime/server/render/astro/factory.ts
+++ b/packages/astro/src/runtime/server/render/astro/factory.ts
@@ -6,7 +6,7 @@ export type AstroFactoryReturnValue = RenderTemplateResult | Response | HeadAndC
// The callback passed to to $$createComponent
export interface AstroComponentFactory {
- (result: any, props: any, slots: any): AstroFactoryReturnValue;
+ (result: any, props: any, slots: any): AstroFactoryReturnValue | Promise<AstroFactoryReturnValue>;
isAstroComponentFactory?: boolean;
moduleId?: string | undefined;
propagation?: PropagationHint;
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index 389ae71ba..2119823c4 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -57,7 +57,7 @@ export class AstroComponentInstance {
await this.init(this.result);
}
- let value: AstroFactoryReturnValue | undefined = this.returnValue;
+ let value: Promise<AstroFactoryReturnValue> | AstroFactoryReturnValue | undefined = this.returnValue;
if (isPromise(value)) {
value = await value;
}
diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
index 36fee4e13..157f0c603 100644
--- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts
+++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
@@ -10,7 +10,7 @@ import type {
} from '../@types/astro.js';
import { getInfoOutput } from '../cli/info/index.js';
import type { HeadElements } from '../core/base-pipeline.js';
-import { ASTRO_VERSION } from '../core/constants.js';
+import { ASTRO_VERSION, DEFAULT_404_COMPONENT } from '../core/constants.js';
import { enhanceViteSSRError } from '../core/errors/dev/index.js';
import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
@@ -23,6 +23,7 @@ import { getStylesForURL } from './css.js';
import { getComponentMetadata } from './metadata.js';
import { createResolve } from './resolve.js';
import { getScriptsForURL } from './scripts.js';
+import { default404Page } from './response.js';
export class DevPipeline extends Pipeline {
// renderers are loaded on every request,
@@ -136,6 +137,9 @@ export class DevPipeline extends Pipeline {
async preload(filePath: URL) {
const { loader } = this;
+ if (filePath.href === new URL(DEFAULT_404_COMPONENT, this.config.root).href) {
+ return { default: default404Page } as any as ComponentInstance
+ }
// Important: This needs to happen first, in case a renderer provides polyfills.
const renderers__ = this.settings.renderers.map((r) => loadRenderer(r, loader));
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index c1f6aa42e..be0f6a8ed 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -17,6 +17,7 @@ import { recordServerError } from './error.js';
import { DevPipeline } from './pipeline.js';
import { handleRequest } from './request.js';
import { setRouteError } from './server-state.js';
+import { ensure404Route } from '../core/routing/astro-designed-error-pages.js';
export interface AstroPluginOptions {
settings: AstroSettings;
@@ -35,15 +36,15 @@ export default function createVitePluginAstroServer({
const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings);
const pipeline = DevPipeline.create({ loader, logger, manifest, settings });
- let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logger);
+ let manifestData: ManifestData = ensure404Route(createRouteManifest({ settings, fsMod }, logger));
const controller = createController({ loader });
const localStorage = new AsyncLocalStorage();
-
+
/** rebuild the route cache + manifest, as needed. */
function rebuildManifest(needsManifestRebuild: boolean) {
pipeline.clearRouteCache();
if (needsManifestRebuild) {
- manifestData = createRouteManifest({ settings }, logger);
+ manifestData = ensure404Route(createRouteManifest({ settings }, logger));
}
}
// Rebuild route manifest on file change, if needed.
diff --git a/packages/astro/src/vite-plugin-astro-server/response.ts b/packages/astro/src/vite-plugin-astro-server/response.ts
index c6e034aef..6dccc753f 100644
--- a/packages/astro/src/vite-plugin-astro-server/response.ts
+++ b/packages/astro/src/vite-plugin-astro-server/response.ts
@@ -23,6 +23,19 @@ export async function handle404Response(
writeHtmlResponse(res, 404, html);
}
+export async function default404Page(
+ { pathname }: { pathname: string }
+) {
+ return new Response(notFoundTemplate({
+ statusCode: 404,
+ title: 'Not found',
+ tabTitle: '404: Not Found',
+ pathname,
+ }), { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
+}
+// mark the function as an AstroComponentFactory for the rendering internals
+default404Page.isAstroComponentFactory = true;
+
export async function handle500Response(
loader: ModuleLoader,
res: http.ServerResponse,
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 1c533dbc8..a38130dca 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -1,6 +1,6 @@
import type http from 'node:http';
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro.js';
-import { REROUTE_DIRECTIVE_HEADER, clientLocalsSymbol } from '../core/constants.js';
+import { DEFAULT_404_COMPONENT, REROUTE_DIRECTIVE_HEADER, clientLocalsSymbol } from '../core/constants.js';
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
import { req } from '../core/messages.js';
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
@@ -11,7 +11,7 @@ import { matchAllRoutes } from '../core/routing/index.js';
import { normalizeTheLocale } from '../i18n/index.js';
import { getSortedPreloadedMatches } from '../prerender/routing.js';
import type { DevPipeline } from './pipeline.js';
-import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
+import { default404Page, handle404Response, writeSSRResult, writeWebResponse } from './response.js';
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
...args: any
@@ -94,6 +94,19 @@ export async function matchRoute(
}
const custom404 = getCustom404Route(manifestData);
+
+ if (custom404 && custom404.component === DEFAULT_404_COMPONENT) {
+ const component: ComponentInstance = {
+ default: default404Page
+ }
+ return {
+ route: custom404,
+ filePath: new URL(`file://${custom404.component}`),
+ resolvedPathname: pathname,
+ preloadedComponent: component,
+ mod: component,
+ }
+ }
if (custom404) {
const filePath = new URL(`./${custom404.component}`, config.root);
diff --git a/packages/astro/test/astro-dev-headers.test.js b/packages/astro/test/astro-dev-headers.test.js
index e119e365a..ec7999c33 100644
--- a/packages/astro/test/astro-dev-headers.test.js
+++ b/packages/astro/test/astro-dev-headers.test.js
@@ -31,10 +31,10 @@ describe('Astro dev headers', () => {
assert.equal(Object.fromEntries(result.headers)['x-astro'], headers['x-astro']);
});
- it('does not return custom headers for invalid URLs', async () => {
+ it('returns custom headers in the default 404 response', async () => {
const result = await fixture.fetch('/bad-url');
assert.equal(result.status, 404);
- assert.equal(Object.fromEntries(result.headers).hasOwnProperty('x-astro'), false);
+ assert.equal(Object.fromEntries(result.headers).hasOwnProperty('x-astro'), true);
});
});
});
diff --git a/packages/astro/test/fixtures/virtual-routes/astro.config.js b/packages/astro/test/fixtures/virtual-routes/astro.config.js
new file mode 100644
index 000000000..37a31e918
--- /dev/null
+++ b/packages/astro/test/fixtures/virtual-routes/astro.config.js
@@ -0,0 +1,6 @@
+import testAdapter from '../../test-adapter.js';
+
+export default {
+ output: 'server',
+ adapter: testAdapter(),
+};
diff --git a/packages/astro/test/fixtures/virtual-routes/package.json b/packages/astro/test/fixtures/virtual-routes/package.json
new file mode 100644
index 000000000..1e11618c7
--- /dev/null
+++ b/packages/astro/test/fixtures/virtual-routes/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@test/virtual-routes",
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+ }
+ \ No newline at end of file
diff --git a/packages/astro/test/fixtures/virtual-routes/src/middleware.js b/packages/astro/test/fixtures/virtual-routes/src/middleware.js
new file mode 100644
index 000000000..96b626601
--- /dev/null
+++ b/packages/astro/test/fixtures/virtual-routes/src/middleware.js
@@ -0,0 +1,8 @@
+export function onRequest (context, next) {
+ if (context.request.url.includes('/virtual')) {
+ return new Response('<span>Virtual!!</span>', {
+ status: 200,
+ });
+ }
+ return next()
+}
diff --git a/packages/astro/test/units/routing/trailing-slash.test.js b/packages/astro/test/units/routing/trailing-slash.test.js
index 8bbc33f19..a9e8fe945 100644
--- a/packages/astro/test/units/routing/trailing-slash.test.js
+++ b/packages/astro/test/units/routing/trailing-slash.test.js
@@ -54,7 +54,8 @@ describe('trailingSlash', () => {
url: '/api',
});
container.handle(req, res);
- assert.equal(await text(), '');
+ const html = await text();
+ assert.equal(html.includes(`<span class="statusMessage">Not found</span>`), true);
assert.equal(res.statusCode, 404);
});
});
diff --git a/packages/astro/test/virtual-routes.test.js b/packages/astro/test/virtual-routes.test.js
new file mode 100644
index 000000000..2c9286e8e
--- /dev/null
+++ b/packages/astro/test/virtual-routes.test.js
@@ -0,0 +1,30 @@
+import assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { loadFixture } from './test-utils.js';
+
+describe('virtual routes - dev', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/virtual-routes/',
+ });
+ await fixture.build();
+ });
+
+ it('should render a virtual route - dev', async () => {
+ const devServer = await fixture.startDevServer();
+ const response = await fixture.fetch('/virtual');
+ const html = await response.text();
+ assert.equal(html.includes('Virtual!!'), true);
+ await devServer.stop();
+ });
+
+ it('should render a virtual route - app', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const response = await app.render(new Request('https://example.com/virtual'));
+ const html = await response.text();
+ assert.equal(html.includes('Virtual!!'), true);
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 950710d7a..f9f8f4904 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3731,6 +3731,12 @@ importers:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/virtual-routes:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+
packages/astro/test/fixtures/vue-component:
dependencies:
'@astrojs/vue':