astro 5.17.2 → 5.17.3

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.
@@ -163,6 +163,9 @@ function getActionContext(context) {
163
163
  try {
164
164
  input = await parseRequestBody(context.request);
165
165
  } catch (e) {
166
+ if (e instanceof ActionError) {
167
+ return { data: void 0, error: e };
168
+ }
166
169
  if (e instanceof TypeError) {
167
170
  return { data: void 0, error: new ActionError({ code: "UNSUPPORTED_MEDIA_TYPE" }) };
168
171
  }
@@ -206,18 +209,74 @@ function getCallerInfo(ctx) {
206
209
  }
207
210
  return void 0;
208
211
  }
212
+ const DEFAULT_ACTION_BODY_SIZE_LIMIT = 1024 * 1024;
209
213
  async function parseRequestBody(request) {
210
214
  const contentType = request.headers.get("content-type");
211
- const contentLength = request.headers.get("Content-Length");
215
+ const contentLengthHeader = request.headers.get("content-length");
216
+ const contentLength = contentLengthHeader ? Number.parseInt(contentLengthHeader, 10) : void 0;
217
+ const hasContentLength = typeof contentLength === "number" && Number.isFinite(contentLength);
212
218
  if (!contentType) return void 0;
219
+ if (hasContentLength && contentLength > DEFAULT_ACTION_BODY_SIZE_LIMIT) {
220
+ throw new ActionError({
221
+ code: "CONTENT_TOO_LARGE",
222
+ message: `Request body exceeds ${DEFAULT_ACTION_BODY_SIZE_LIMIT} bytes`
223
+ });
224
+ }
213
225
  if (hasContentType(contentType, formContentTypes)) {
226
+ if (!hasContentLength) {
227
+ const body = await readRequestBodyWithLimit(request.clone(), DEFAULT_ACTION_BODY_SIZE_LIMIT);
228
+ const formRequest = new Request(request.url, {
229
+ method: request.method,
230
+ headers: request.headers,
231
+ body: toArrayBuffer(body)
232
+ });
233
+ return await formRequest.formData();
234
+ }
214
235
  return await request.clone().formData();
215
236
  }
216
237
  if (hasContentType(contentType, ["application/json"])) {
217
- return contentLength === "0" ? void 0 : await request.clone().json();
238
+ if (contentLength === 0) return void 0;
239
+ if (!hasContentLength) {
240
+ const body = await readRequestBodyWithLimit(request.clone(), DEFAULT_ACTION_BODY_SIZE_LIMIT);
241
+ if (body.byteLength === 0) return void 0;
242
+ return JSON.parse(new TextDecoder().decode(body));
243
+ }
244
+ return await request.clone().json();
218
245
  }
219
246
  throw new TypeError("Unsupported content type");
220
247
  }
248
+ async function readRequestBodyWithLimit(request, limit) {
249
+ if (!request.body) return new Uint8Array();
250
+ const reader = request.body.getReader();
251
+ const chunks = [];
252
+ let received = 0;
253
+ while (true) {
254
+ const { done, value } = await reader.read();
255
+ if (done) break;
256
+ if (value) {
257
+ received += value.byteLength;
258
+ if (received > limit) {
259
+ throw new ActionError({
260
+ code: "CONTENT_TOO_LARGE",
261
+ message: `Request body exceeds ${limit} bytes`
262
+ });
263
+ }
264
+ chunks.push(value);
265
+ }
266
+ }
267
+ const buffer = new Uint8Array(received);
268
+ let offset = 0;
269
+ for (const chunk of chunks) {
270
+ buffer.set(chunk, offset);
271
+ offset += chunk.byteLength;
272
+ }
273
+ return buffer;
274
+ }
275
+ function toArrayBuffer(buffer) {
276
+ const copy = new Uint8Array(buffer.byteLength);
277
+ copy.set(buffer);
278
+ return copy.buffer;
279
+ }
221
280
  export {
222
281
  defineAction,
223
282
  formDataToObject,
@@ -1,7 +1,10 @@
1
1
  import CachePolicy from "http-cache-semantics";
2
2
  async function loadRemoteImage(src) {
3
3
  const req = new Request(src);
4
- const res = await fetch(req);
4
+ const res = await fetch(req, { redirect: "manual" });
5
+ if (res.status >= 300 && res.status < 400) {
6
+ throw new Error(`Failed to load remote image ${src}. The request was redirected.`);
7
+ }
5
8
  if (!res.ok) {
6
9
  throw new Error(
7
10
  `Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`
@@ -22,7 +25,10 @@ async function revalidateRemoteImage(src, revalidationData) {
22
25
  ...revalidationData.lastModified && { "If-Modified-Since": revalidationData.lastModified }
23
26
  };
24
27
  const req = new Request(src, { headers, cache: "no-cache" });
25
- const res = await fetch(req);
28
+ const res = await fetch(req, { redirect: "manual" });
29
+ if (res.status >= 300 && res.status < 400) {
30
+ throw new Error(`Failed to revalidate cached remote image ${src}. The request was redirected.`);
31
+ }
26
32
  if (!res.ok && res.status !== 304) {
27
33
  throw new Error(
28
34
  `Failed to revalidate cached remote image ${src}. The request did not return a 200 OK / 304 NOT MODIFIED response. (received ${res.status} ${res.statusText})`
@@ -8,8 +8,12 @@ async function loadRemoteImage(src, headers) {
8
8
  try {
9
9
  const res = await fetch(src, {
10
10
  // Forward all headers from the original request
11
- headers
11
+ headers,
12
+ redirect: "manual"
12
13
  });
14
+ if (res.status >= 300 && res.status < 400) {
15
+ return void 0;
16
+ }
13
17
  if (!res.ok) {
14
18
  return void 0;
15
19
  }
@@ -6,7 +6,10 @@ import { getConfiguredImageService } from "../internal.js";
6
6
  import { etag } from "../utils/etag.js";
7
7
  async function loadRemoteImage(src) {
8
8
  try {
9
- const res = await fetch(src);
9
+ const res = await fetch(src, { redirect: "manual" });
10
+ if (res.status >= 300 && res.status < 400) {
11
+ return void 0;
12
+ }
10
13
  if (!res.ok) {
11
14
  return void 0;
12
15
  }
@@ -1,4 +1,5 @@
1
1
  import { isRemotePath } from "@astrojs/internal-helpers/path";
2
+ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
2
3
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
3
4
  import { DEFAULT_HASH_PROPS } from "./consts.js";
4
5
  import {
@@ -58,13 +59,21 @@ async function getImage(options, imageConfig) {
58
59
  };
59
60
  let originalWidth;
60
61
  let originalHeight;
61
- if (options.inferSize && isRemoteImage(resolvedOptions.src) && isRemotePath(resolvedOptions.src)) {
62
- const result = await inferRemoteSize(resolvedOptions.src);
63
- resolvedOptions.width ??= result.width;
64
- resolvedOptions.height ??= result.height;
65
- originalWidth = result.width;
66
- originalHeight = result.height;
62
+ if (options.inferSize) {
67
63
  delete resolvedOptions.inferSize;
64
+ if (isRemoteImage(resolvedOptions.src) && isRemotePath(resolvedOptions.src)) {
65
+ if (!isRemoteAllowed(resolvedOptions.src, imageConfig)) {
66
+ throw new AstroError({
67
+ ...AstroErrorData.RemoteImageNotAllowed,
68
+ message: AstroErrorData.RemoteImageNotAllowed.message(resolvedOptions.src)
69
+ });
70
+ }
71
+ const result = await inferRemoteSize(resolvedOptions.src, imageConfig);
72
+ resolvedOptions.width ??= result.width;
73
+ resolvedOptions.height ??= result.height;
74
+ originalWidth = result.width;
75
+ originalHeight = result.height;
76
+ }
68
77
  }
69
78
  const originalFilePath = isESMImportedImage(resolvedOptions.src) ? resolvedOptions.src.fsPath : void 0;
70
79
  const clonedSrc = isESMImportedImage(resolvedOptions.src) ? (
@@ -1,9 +1,13 @@
1
+ import type { AstroConfig } from '../../types/public/config.js';
1
2
  import type { ImageMetadata } from '../types.js';
3
+ type RemoteImageConfig = Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>;
2
4
  /**
3
5
  * Infers the dimensions of a remote image by streaming its data and analyzing it progressively until sufficient metadata is available.
4
6
  *
5
7
  * @param {string} url - The URL of the remote image from which to infer size metadata.
8
+ * @param {RemoteImageConfig} [imageConfig] - Optional image config used to validate remote allowlists.
6
9
  * @return {Promise<Omit<ImageMetadata, 'src' | 'fsPath'>>} Returns a promise that resolves to an object containing the image dimensions metadata excluding `src` and `fsPath`.
7
10
  * @throws {AstroError} Thrown when the fetching fails or metadata cannot be extracted.
8
11
  */
9
- export declare function inferRemoteSize(url: string): Promise<Omit<ImageMetadata, 'src' | 'fsPath'>>;
12
+ export declare function inferRemoteSize(url: string, imageConfig?: RemoteImageConfig): Promise<Omit<ImageMetadata, 'src' | 'fsPath'>>;
13
+ export {};
@@ -1,7 +1,39 @@
1
+ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
1
2
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
2
3
  import { imageMetadata } from "./metadata.js";
3
- async function inferRemoteSize(url) {
4
- const response = await fetch(url);
4
+ async function inferRemoteSize(url, imageConfig) {
5
+ if (!URL.canParse(url)) {
6
+ throw new AstroError({
7
+ ...AstroErrorData.FailedToFetchRemoteImageDimensions,
8
+ message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url)
9
+ });
10
+ }
11
+ const allowlistConfig = imageConfig ? {
12
+ domains: imageConfig.domains ?? [],
13
+ remotePatterns: imageConfig.remotePatterns ?? []
14
+ } : void 0;
15
+ if (!allowlistConfig) {
16
+ const parsedUrl = new URL(url);
17
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
18
+ throw new AstroError({
19
+ ...AstroErrorData.FailedToFetchRemoteImageDimensions,
20
+ message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url)
21
+ });
22
+ }
23
+ }
24
+ if (allowlistConfig && !isRemoteAllowed(url, allowlistConfig)) {
25
+ throw new AstroError({
26
+ ...AstroErrorData.RemoteImageNotAllowed,
27
+ message: AstroErrorData.RemoteImageNotAllowed.message(url)
28
+ });
29
+ }
30
+ const response = await fetch(url, { redirect: "manual" });
31
+ if (response.status >= 300 && response.status < 400) {
32
+ throw new AstroError({
33
+ ...AstroErrorData.FailedToFetchRemoteImageDimensions,
34
+ message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url)
35
+ });
36
+ }
5
37
  if (!response.body || !response.ok) {
6
38
  throw new AstroError({
7
39
  ...AstroErrorData.FailedToFetchRemoteImageDimensions,
@@ -101,11 +101,11 @@ function assets({ fs, settings, sync, logger }) {
101
101
  if (id === resolvedVirtualModuleId) {
102
102
  return {
103
103
  code: `
104
- export { getConfiguredImageService, isLocalService } from "astro/assets";
105
- import { getImage as getImageInternal } from "astro/assets";
106
- export { default as Image } from "astro/components/${imageComponentPrefix}Image.astro";
107
- export { default as Picture } from "astro/components/${imageComponentPrefix}Picture.astro";
108
- export { inferRemoteSize } from "astro/assets/utils/inferRemoteSize.js";
104
+ export { getConfiguredImageService, isLocalService } from "astro/assets";
105
+ import { getImage as getImageInternal } from "astro/assets";
106
+ import { inferRemoteSize as inferRemoteSizeInternal } from "astro/assets/utils/inferRemoteSize.js";
107
+ export { default as Image } from "astro/components/${imageComponentPrefix}Image.astro";
108
+ export { default as Picture } from "astro/components/${imageComponentPrefix}Picture.astro";
109
109
 
110
110
  export { default as Font } from "astro/components/Font.astro";
111
111
  export * from "${RUNTIME_VIRTUAL_MODULE_ID}";
@@ -126,6 +126,9 @@ function assets({ fs, settings, sync, logger }) {
126
126
  enumerable: false,
127
127
  configurable: true,
128
128
  });
129
+ export const inferRemoteSize = async (url) => {
130
+ return inferRemoteSizeInternal(url, imageConfig)
131
+ }
129
132
  // This is used by the @astrojs/node integration to locate images.
130
133
  // It's unused on other platforms, but on some platforms like Netlify (and presumably also Vercel)
131
134
  // new URL("dist/...") is interpreted by the bundler as a signal to include that directory
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "5.17.2";
3
+ version = "5.17.3";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -164,7 +164,7 @@ ${contentConfig.error.message}`);
164
164
  logger.info("Content config changed");
165
165
  shouldClear = true;
166
166
  }
167
- if (previousAstroVersion && previousAstroVersion !== "5.17.2") {
167
+ if (previousAstroVersion && previousAstroVersion !== "5.17.3") {
168
168
  logger.info("Astro version changed");
169
169
  shouldClear = true;
170
170
  }
@@ -172,8 +172,8 @@ ${contentConfig.error.message}`);
172
172
  logger.info("Clearing content store");
173
173
  this.#store.clearAll();
174
174
  }
175
- if ("5.17.2") {
176
- await this.#store.metaStore().set("astro-version", "5.17.2");
175
+ if ("5.17.3") {
176
+ await this.#store.metaStore().set("astro-version", "5.17.3");
177
177
  }
178
178
  if (currentConfigDigest) {
179
179
  await this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "5.17.2";
1
+ const ASTRO_VERSION = "5.17.3";
2
2
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
3
3
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
4
4
  const REWRITE_DIRECTIVE_HEADER_VALUE = "yes";
@@ -22,7 +22,7 @@ async function dev(inlineConfig) {
22
22
  await telemetry.record([]);
23
23
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
24
24
  const logger = restart.container.logger;
25
- const currentVersion = "5.17.2";
25
+ const currentVersion = "5.17.3";
26
26
  const isPrerelease = currentVersion.includes("-");
27
27
  if (!isPrerelease) {
28
28
  try {
@@ -491,6 +491,19 @@ export declare const FailedToFetchRemoteImageDimensions: {
491
491
  message: (imageURL: string) => string;
492
492
  hint: string;
493
493
  };
494
+ /**
495
+ * @docs
496
+ * @message
497
+ * Remote image `IMAGE_URL` is not allowed by your image configuration.
498
+ * @description
499
+ * The remote image URL does not match your configured `image.domains` or `image.remotePatterns`.
500
+ */
501
+ export declare const RemoteImageNotAllowed: {
502
+ name: string;
503
+ title: string;
504
+ message: (imageURL: string) => string;
505
+ hint: string;
506
+ };
494
507
  /**
495
508
  * @docs
496
509
  * @description
@@ -175,6 +175,12 @@ const FailedToFetchRemoteImageDimensions = {
175
175
  message: (imageURL) => `Failed to get the dimensions for ${imageURL}.`,
176
176
  hint: "Verify your remote image URL is accurate, and that you are not using `inferSize` with a file located in your `public/` folder."
177
177
  };
178
+ const RemoteImageNotAllowed = {
179
+ name: "RemoteImageNotAllowed",
180
+ title: "Remote image is not allowed",
181
+ message: (imageURL) => `Remote image ${imageURL} is not allowed by your image configuration.`,
182
+ hint: "Update `image.domains` or `image.remotePatterns`, or remove `inferSize` for this image."
183
+ };
178
184
  const UnsupportedImageFormat = {
179
185
  name: "UnsupportedImageFormat",
180
186
  title: "Unsupported image format",
@@ -832,6 +838,7 @@ export {
832
838
  PrerenderDynamicEndpointPathCollide,
833
839
  PrerenderRouteConflict,
834
840
  RedirectWithNoLocation,
841
+ RemoteImageNotAllowed,
835
842
  RenderUndefinedEntryError,
836
843
  ReservedSlotName,
837
844
  ResponseSentError,
@@ -38,7 +38,7 @@ function serverStart({
38
38
  host,
39
39
  base
40
40
  }) {
41
- const version = "5.17.2";
41
+ const version = "5.17.3";
42
42
  const localPrefix = `${dim("\u2503")} Local `;
43
43
  const networkPrefix = `${dim("\u2503")} Network `;
44
44
  const emptyPrefix = " ".repeat(11);
@@ -275,7 +275,7 @@ function printHelp({
275
275
  message.push(
276
276
  linebreak(),
277
277
  ` ${bgGreen(black(` ${commandName} `))} ${green(
278
- `v${"5.17.2"}`
278
+ `v${"5.17.3"}`
279
279
  )} ${headline}`
280
280
  );
281
281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "5.17.2",
3
+ "version": "5.17.3",
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",
@@ -113,7 +113,7 @@
113
113
  "dlv": "^1.1.3",
114
114
  "dset": "^3.1.4",
115
115
  "es-module-lexer": "^1.7.0",
116
- "esbuild": "^0.27.0",
116
+ "esbuild": "^0.27.3",
117
117
  "estree-walker": "^3.0.3",
118
118
  "flattie": "^1.1.1",
119
119
  "fontace": "~0.4.0",
@@ -154,8 +154,8 @@
154
154
  "zod-to-json-schema": "^3.25.1",
155
155
  "zod-to-ts": "^1.2.0",
156
156
  "@astrojs/internal-helpers": "0.7.5",
157
- "@astrojs/markdown-remark": "6.3.10",
158
- "@astrojs/telemetry": "3.3.0"
157
+ "@astrojs/telemetry": "3.3.0",
158
+ "@astrojs/markdown-remark": "6.3.10"
159
159
  },
160
160
  "optionalDependencies": {
161
161
  "sharp": "^0.34.0"