next/image option to disable fs cache #25846
Replies: 7 comments 5 replies
-
I was able to accomplish this using a custom loader. I basically copied the source code from next/image into an API route and removed all the disk caching. Warning: This does not use any of the config values from next-config.js.
customImageLoader.js export default function customImageLoader({ src, width, quality }) {
return `/api/image?url=${src}&w=${width}&q=${quality || 75}`;
} /pages/api/image.js import { mediaType } from "@hapi/accept";
import sharp from "sharp";
const AVIF = "image/avif";
const WEBP = "image/webp";
const PNG = "image/png";
const JPEG = "image/jpeg";
const GIF = "image/gif";
const SVG = "image/svg+xml";
export default async function image(req, res) {
const { url, w, q } = req.query;
const mimeType = getSupportedMimeType(["image/webp"], req.headers.accept);
let href;
if (!url) {
res.statusCode = 400;
res.end('"url" parameter is required');
return { finished: true };
} else if (Array.isArray(url)) {
res.statusCode = 400;
res.end('"url" parameter cannot be an array');
return { finished: true };
}
let hrefParsed;
try {
hrefParsed = new URL(url);
href = hrefParsed.toString();
} catch (_error) {
res.statusCode = 400;
res.end('"url" parameter is invalid');
return { finished: true };
}
if (!["http:", "https:"].includes(hrefParsed.protocol)) {
res.statusCode = 400;
res.end('"url" parameter is invalid');
return { finished: true };
}
if (!w) {
res.statusCode = 400;
res.end('"w" parameter (width) is required');
return { finished: true };
} else if (Array.isArray(w)) {
res.statusCode = 400;
res.end('"w" parameter (width) cannot be an array');
return { finished: true };
}
if (!q) {
res.statusCode = 400;
res.end('"q" parameter (quality) is required');
return { finished: true };
} else if (Array.isArray(q)) {
res.statusCode = 400;
res.end('"q" parameter (quality) cannot be an array');
return { finished: true };
}
const width = parseInt(w, 10);
if (!width || isNaN(width)) {
res.statusCode = 400;
res.end('"w" parameter (width) must be a number greater than 0');
return { finished: true };
}
const sizes = [
16, 32, 48, 64, 96, 128, 256, 384, 640, 750,
828, 1080, 1200, 1920, 2048, 3840,
];
if (!sizes.includes(width)) {
res.statusCode = 400;
res.end(`"w" parameter (width) of ${width} is not allowed`);
return { finished: true };
}
const quality = parseInt(q);
if (isNaN(quality) || quality < 1 || quality > 100) {
res.statusCode = 400;
res.end('"q" parameter (quality) must be a number between 1 and 100');
return { finished: true };
}
try {
let upstreamBuffer;
let upstreamType;
const upstreamRes = await fetch(href);
if (!upstreamRes.ok) {
res.statusCode = upstreamRes.status;
res.end('"url" parameter is valid but upstream response is invalid');
return { finished: true };
}
res.statusCode = upstreamRes.status;
upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer());
upstreamType =
detectContentType(upstreamBuffer) ||
upstreamRes.headers.get("Content-Type");
if (upstreamType) {
if (!upstreamType.startsWith("image/")) {
res.statusCode = 400;
res.end("The requested resource isn't a valid image.");
return { finished: true };
}
}
let contentType;
if (mimeType) {
contentType = mimeType;
} else if (upstreamType?.startsWith("image/")) {
contentType = upstreamType;
} else {
contentType = JPEG;
}
let optimizedBuffer;
// Begin sharp transformation logic
const transformer = sharp(upstreamBuffer);
transformer.rotate();
const { width: metaWidth } = await transformer.metadata();
if (metaWidth && metaWidth > width) {
transformer.resize(width);
}
if (contentType === WEBP) {
transformer.webp({ quality });
} else if (contentType === PNG) {
transformer.png({ quality });
} else if (contentType === JPEG) {
transformer.jpeg({ quality });
}
optimizedBuffer = await transformer.toBuffer();
// End sharp transformation logic
if (optimizedBuffer) {
sendResponse(req, res, url, contentType, optimizedBuffer);
} else {
throw new Error("Unable to optimize buffer");
}
return { finished: true };
} catch (error) {
res.statusCode = 500;
res.end(error.toString());
return { finished: false };
}
}
function setResponseHeaders(req, res, url, contentType) {
if (contentType) {
res.setHeader("Content-Type", contentType);
}
res.setHeader("Content-Security-Policy", `script-src 'none'; sandbox;`);
return { finished: false };
}
function sendResponse(req, res, url, contentType, buffer) {
const result = setResponseHeaders(req, res, url, contentType);
if (!result.finished) {
res.end(buffer);
}
}
function getSupportedMimeType(options, accept = "") {
const mimeType = mediaType(accept, options);
return accept.includes(mimeType) ? mimeType : "";
}
/**
* Inspects the first few bytes of a buffer to determine if
* it matches the "magic number" of known file signatures.
* https://en.wikipedia.org/wiki/List_of_file_signatures
*/
export function detectContentType(buffer) {
if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
return JPEG;
}
if (
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every(
(b, i) => buffer[i] === b
)
) {
return PNG;
}
if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
return GIF;
}
if (
[0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every(
(b, i) => !b || buffer[i] === b
)
) {
return WEBP;
}
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
return SVG;
}
if (
[0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every(
(b, i) => !b || buffer[i] === b
)
) {
return AVIF;
}
return null;
} |
Beta Was this translation helpful? Give feedback.
-
I had to deal with some flacky tests in playwright, so this would be an interesting idea. PS: It might also be a bug (maybe fixed). But since Nextjs 12.0.9 and 12.0.10, I had few errors from time to time (in CI & in local). sharp is at 0.30.1. I haven't notice this error on <= 12.0.8
Some history in belgattitude/nextjs-monorepo-example#1206 |
Beta Was this translation helpful? Give feedback.
-
Would be useful. Currently working on a read-only fs. |
Beta Was this translation helpful? Give feedback.
-
I encountered a similar situation while creating a read-only filesystem Docker container for better security. Next.js kept throwing errors like this: |
Beta Was this translation helpful? Give feedback.
-
Looks like there are 24 upvotes now. If someone wants to create a PR, I can take a look, thanks! |
Beta Was this translation helpful? Give feedback.
-
Having an option to disable image cache would be useful. Example: <Image
src="..."
alt="..."
width="..."
height="..."
cache={false}
/> |
Beta Was this translation helpful? Give feedback.
-
@styfle is there any chance to squeeze this config to the next release or sometime in the future? |
Beta Was this translation helpful? Give feedback.
-
Describe the feature you'd like to request
To avoid heavy I/O and filling disks would be useful an option like
module.exports = { images: { writeToCacheDir: false, }, }
to skip this operation
next.js/packages/next/next-server/server/image-optimizer.ts
Line 335 in 38fa5ca
Describe the solution you'd like
The proposed
writeToCacheDir
option would be useful when Next.js runs behind CDN.In this scenario it will up to the CDN manage the cache for _next/image* requests.
Describe alternatives you've considered
Setup an external cronjob to cleanup dist/cache/images/* to clean up the disk.
Beta Was this translation helpful? Give feedback.
All reactions