Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/long-chefs-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/markdown-remark': patch
---

Fixes an issue where the use of the `Code` component would result in an unexpected error.
27 changes: 27 additions & 0 deletions .changeset/many-cars-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'astro': minor
---

Adds a new `middlewareMode` adapter feature to replace the previous `edgeMiddleware` option.

This feature only impacts adapter authors. If your adapter supports `edgeMiddleware`, you should upgrade to the new `middlewareMode` option to specify the middleware mode for your adapter as soon as possible. The `edgeMiddleware` feature is deprecated and will be removed in a future major release.

```diff
export default function createIntegration() {
return {
name: '@example/my-adapter',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@example/my-adapter',
serverEntrypoint: '@example/my-adapter/server.js',
adapterFeatures: {
- edgeMiddleware: true
+ middlewareMode: 'edge'
}
});
},
},
};
}
```
16 changes: 16 additions & 0 deletions .changeset/social-jeans-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@astrojs/netlify': minor
---

Adds new `middlewareMode` adapter feature and deprecates `edgeMiddleware` option

The `edgeMiddleware` option is now deprecated and will be removed in a future major release, so users should transition to using the new `middlewareMode` feature as soon as possible.

```diff
export default defineConfig({
adapter: netlify({
- edgeMiddleware: true
+ middlewareMode: 'edge'
})
})
```
16 changes: 16 additions & 0 deletions .changeset/three-sheep-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@astrojs/vercel': minor
---

Adds new `middlewareMode` adapter feature and deprecates `edgeMiddleware` option

The `edgeMiddleware` option is now deprecated and will be removed in a future release, so users should transition to using the new `middlewareMode` feature as soon as possible.

```diff
export default defineConfig({
adapter: vercel({
- edgeMiddleware: true
+ middlewareMode: 'edge'
})
})
```
5 changes: 5 additions & 0 deletions .changeset/yellow-cycles-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/cloudflare': patch
'@astrojs/node': patch
---
Refactors to use `middlewareMode` adapter feature (set to `classic`)
19 changes: 19 additions & 0 deletions .changeset/young-cougars-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@astrojs/node': patch
'astro': patch
---

Adds a new `security.actionBodySizeLimit` option to configure the maximum size of Astro Actions request bodies.

This lets you increase the default 1 MB limit when your actions need to accept larger payloads. For example, actions that handle file uploads or large JSON payloads can now opt in to a higher limit.

If you do not set this option, Astro continues to enforce the 1 MB default to help prevent abuse.

```js
// astro.config.mjs
export default defineConfig({
security: {
actionBodySizeLimit: 10 * 1024 * 1024 // set to 10 MB
}
})
```
15 changes: 7 additions & 8 deletions packages/astro/src/actions/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,10 @@ export function getActionContext(context: APIContext): AstroActionContext {
throw error;
}

const bodySizeLimit = pipeline.manifest.actionBodySizeLimit;
let input;
try {
input = await parseRequestBody(context.request);
input = await parseRequestBody(context.request, bodySizeLimit);
} catch (e) {
if (e instanceof ActionError) {
return { data: undefined, error: e };
Expand Down Expand Up @@ -253,24 +254,22 @@ function getCallerInfo(ctx: APIContext) {
return undefined;
}

const DEFAULT_ACTION_BODY_SIZE_LIMIT = 1024 * 1024;

async function parseRequestBody(request: Request) {
async function parseRequestBody(request: Request, bodySizeLimit: number) {
const contentType = request.headers.get('content-type');
const contentLengthHeader = request.headers.get('content-length');
const contentLength = contentLengthHeader ? Number.parseInt(contentLengthHeader, 10) : undefined;
const hasContentLength = typeof contentLength === 'number' && Number.isFinite(contentLength);

if (!contentType) return undefined;
if (hasContentLength && contentLength > DEFAULT_ACTION_BODY_SIZE_LIMIT) {
if (hasContentLength && contentLength > bodySizeLimit) {
throw new ActionError({
code: 'CONTENT_TOO_LARGE',
message: `Request body exceeds ${DEFAULT_ACTION_BODY_SIZE_LIMIT} bytes`,
message: `Request body exceeds ${bodySizeLimit} bytes`,
});
}
if (hasContentType(contentType, formContentTypes)) {
if (!hasContentLength) {
const body = await readRequestBodyWithLimit(request.clone(), DEFAULT_ACTION_BODY_SIZE_LIMIT);
const body = await readRequestBodyWithLimit(request.clone(), bodySizeLimit);
const formRequest = new Request(request.url, {
method: request.method,
headers: request.headers,
Expand All @@ -283,7 +282,7 @@ async function parseRequestBody(request: Request) {
if (hasContentType(contentType, ['application/json'])) {
if (contentLength === 0) return undefined;
if (!hasContentLength) {
const body = await readRequestBodyWithLimit(request.clone(), DEFAULT_ACTION_BODY_SIZE_LIMIT);
const body = await readRequestBodyWithLimit(request.clone(), bodySizeLimit);
if (body.byteLength === 0) return undefined;
return JSON.parse(new TextDecoder().decode(body));
}
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function createManifest(
compressHTML: manifest?.compressHTML ?? ASTRO_CONFIG_DEFAULTS.compressHTML,
assetsDir: manifest?.assetsDir ?? ASTRO_CONFIG_DEFAULTS.build.assets,
serverLike: manifest?.serverLike ?? true,
middlewareMode: manifest?.middlewareMode ?? 'classic',
assets: manifest?.assets ?? new Set(),
assetsPrefix: manifest?.assetsPrefix ?? undefined,
entryModules: manifest?.entryModules ?? {},
Expand All @@ -164,6 +165,7 @@ function createManifest(
i18n: manifest?.i18n,
checkOrigin: false,
allowedDomains: manifest?.allowedDomains ?? [],
actionBodySizeLimit: 1024 * 1024,
middleware: manifest?.middleware ?? middlewareInstance,
key: createKey(),
csp: manifest?.csp,
Expand Down Expand Up @@ -267,6 +269,7 @@ type AstroContainerManifest = Pick<
| 'csp'
| 'allowedDomains'
| 'serverLike'
| 'middlewareMode'
| 'assetsDir'
| 'image'
| 'experimentalQueuedRendering'
Expand Down
51 changes: 21 additions & 30 deletions packages/astro/src/content/content-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ import {
} from './utils.js';
import { createWatcherWrapper, type WrappedWatcher } from './watcher.js';

interface ContentLayerOptions {
export interface ContentLayerOptions {
store: MutableDataStore;
settings: AstroSettings;
logger: Logger;
watcher?: FSWatcher;
contentConfigObserver?: ContentObservable;
}

type CollectionLoader<TData> = () =>
Expand All @@ -45,7 +46,7 @@ type CollectionLoader<TData> = () =>
| Record<string, Record<string, unknown>>
| Promise<Record<string, Record<string, unknown>>>;

class ContentLayer {
export class ContentLayer {
#logger: Logger;
#store: MutableDataStore;
#settings: AstroSettings;
Expand All @@ -54,16 +55,24 @@ class ContentLayer {
#unsubscribe?: () => void;
#markdownProcessor?: MarkdownProcessor;
#generateDigest?: (data: Record<string, unknown> | string) => string;
#contentConfigObserver: ContentObservable;

#queue: PQueue;

constructor({ settings, logger, store, watcher }: ContentLayerOptions) {
constructor({
settings,
logger,
store,
watcher,
contentConfigObserver = globalContentConfigObserver,
}: ContentLayerOptions) {
// The default max listeners is 10, which can be exceeded when using a lot of loaders
watcher?.setMaxListeners(50);

this.#logger = logger;
this.#store = store;
this.#settings = settings;
this.#contentConfigObserver = contentConfigObserver;
if (watcher) {
this.#watcher = createWatcherWrapper(watcher);
}
Expand All @@ -82,7 +91,7 @@ class ContentLayer {
*/
watchContentConfig() {
this.#unsubscribe?.();
this.#unsubscribe = globalContentConfigObserver.subscribe(async (ctx) => {
this.#unsubscribe = this.#contentConfigObserver.subscribe(async (ctx) => {
if (ctx.status === 'loaded' && ctx.config.digest !== this.#lastConfigDigest) {
this.sync();
}
Expand Down Expand Up @@ -172,13 +181,13 @@ class ContentLayer {
}

async #doSync(options: RefreshContentOptions) {
let contentConfig = globalContentConfigObserver.get();
let contentConfig = this.#contentConfigObserver.get();
const logger = this.#logger.forkIntegrationLogger('content');

if (contentConfig?.status === 'loading') {
contentConfig = await Promise.race<ReturnType<ContentObservable['get']>>([
new Promise((resolve) => {
const unsub = globalContentConfigObserver.subscribe((ctx) => {
const unsub = this.#contentConfigObserver.subscribe((ctx) => {
unsub();
resolve(ctx);
});
Expand Down Expand Up @@ -230,9 +239,9 @@ class ContentLayer {
this.#lastConfigDigest = currentConfigDigest;

let shouldClear = false;
const previousConfigDigest = await this.#store.metaStore().get('content-config-digest');
const previousAstroConfigDigest = await this.#store.metaStore().get('astro-config-digest');
const previousAstroVersion = await this.#store.metaStore().get('astro-version');
const previousConfigDigest = this.#store.metaStore().get('content-config-digest');
const previousAstroConfigDigest = this.#store.metaStore().get('astro-config-digest');
const previousAstroVersion = this.#store.metaStore().get('astro-version');

if (previousAstroConfigDigest && previousAstroConfigDigest !== astroConfigDigest) {
logger.info('Astro config changed');
Expand All @@ -252,13 +261,13 @@ class ContentLayer {
this.#store.clearAll();
}
if (process.env.ASTRO_VERSION) {
await this.#store.metaStore().set('astro-version', process.env.ASTRO_VERSION);
this.#store.metaStore().set('astro-version', process.env.ASTRO_VERSION);
}
if (currentConfigDigest) {
await this.#store.metaStore().set('content-config-digest', currentConfigDigest);
this.#store.metaStore().set('content-config-digest', currentConfigDigest);
}
if (astroConfigDigest) {
await this.#store.metaStore().set('astro-config-digest', astroConfigDigest);
this.#store.metaStore().set('astro-config-digest', astroConfigDigest);
}

if (!options?.loaders?.length) {
Expand Down Expand Up @@ -459,21 +468,3 @@ async function simpleLoader<TData extends { id: string }>(
export function getDataStoreFile(settings: AstroSettings, isDev: boolean) {
return new URL(DATA_STORE_FILE, isDev ? settings.dotAstroDir : settings.config.cacheDir);
}

function contentLayerSingleton() {
let instance: ContentLayer | null = null;
return {
init: (options: ContentLayerOptions) => {
instance?.dispose();
instance = new ContentLayer(options);
return instance;
},
get: () => instance,
dispose: () => {
instance?.dispose();
instance = null;
},
};
}

export const globalContentLayer = contentLayerSingleton();
30 changes: 30 additions & 0 deletions packages/astro/src/content/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { FSWatcher } from 'vite';
import { ContentLayer } from './content-layer.js';
import type { Logger } from '../core/logger/core.js';
import type { AstroSettings } from '../types/astro.js';
import type { MutableDataStore } from './mutable-data-store.js';

interface ContentLayerOptions {
store: MutableDataStore;
settings: AstroSettings;
logger: Logger;
watcher?: FSWatcher;
}

function contentLayerSingleton() {
let instance: ContentLayer | null = null;
return {
init: (options: ContentLayerOptions) => {
instance?.dispose();
instance = new ContentLayer(options);
return instance;
},
get: () => instance,
dispose: () => {
instance?.dispose();
instance = null;
},
};
}

export const globalContentLayer = contentLayerSingleton();
8 changes: 8 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { LoggerLevel } from '../logger/core.js';
import type { RoutingStrategies } from './common.js';
import type { BaseSessionConfig, SessionDriverFactory } from '../session/types.js';
import type { DevToolbarPlacement } from '../../types/public/toolbar.js';
import type { MiddlewareMode } from '../../types/public/integrations.js';
import type { BaseApp } from './base.js';

type ComponentPath = string;
Expand Down Expand Up @@ -89,6 +90,12 @@ export type SSRManifest = {
* the creation of `dist/client` and `dist/server` folders.
*/
serverLike: boolean;
/**
* The middleware mode determines when and how middleware executes.
* - 'classic' (default): Build-time for prerendered pages, request-time for SSR pages
* - 'edge': Middleware deployed as separate edge function
*/
middlewareMode: MiddlewareMode;
/**
* Map of directive name (e.g. `load`) to the directive script code
*/
Expand All @@ -107,6 +114,7 @@ export type SSRManifest = {
sessionDriver?: () => Promise<{ default: SessionDriverFactory | null }>;
checkOrigin: boolean;
allowedDomains?: Partial<RemotePattern>[];
actionBodySizeLimit: number;
sessionConfig?: SSRManifestSession;
cacheDir: URL;
srcDir: URL;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export abstract class Pipeline {
}
// The middleware can be undefined when using edge middleware.
// This is set to undefined by the plugin-ssr.ts
else if (this.middleware) {
if (this.middleware) {
const middlewareInstance = await this.middleware();
const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN;
const internalMiddlewares = [onRequest];
Expand Down
Loading
Loading