withastro/astro · error · Error
Configured image service is not a local service
Error message
Configured image service is not a local service
What it means
Same guard as the dev endpoint, but in `handleImageRequest` (packages/astro/src/assets/endpoint/shared.ts:45), the shared handler adapters and preview servers use to serve optimized images. Before parsing the request it asserts the configured image service has a `transform` method; a non-local service (passthrough, URL-rewriting CDN service, adapter-injected service) cannot process requests here. Unlike generic.ts, this throw is not wrapped in try/catch, so it propagates as an unhandled server error.
Solutions
- Configure a local service with `transform` (e.g. `astro/assets/services/sharp`) in `astro.config.mjs` for the environment where SSR/preview runs.
- Verify no adapter/integration overrides `image.service` after your config; log or inspect the resolved config with `astro sync`/debug output.
- If you must keep a non-local service, stop routing optimized image requests to the app (use the CDN URLs the service generates, or plain `<img>` tags).
Example fix
// astro.config.mjs — before
export default defineConfig({ image: { service: { entrypoint: 'astro/assets/services/passthrough' } } });
// astro.config.mjs — after
export default defineConfig({ image: { service: { entrypoint: 'astro/assets/services/sharp' } } }); Defensive patterns
Strategy: validation
Validate before calling
// preflight before astro preview / SSR deploy: ensure a transform-capable service
import fs from 'node:fs';
export function assertLocalImageService(configFile = './astro.config.mjs') {
const cfg = fs.readFileSync(configFile, 'utf8');
if (/passthrough/.test(cfg)) {
throw new Error('handleImageRequest requires a local image service; passthrough is configured');
}
} Type guard
function isLocalService(service: unknown): service is { transform: (input: Uint8Array, t: unknown, cfg: unknown) => Promise<{ data: Uint8Array; format: string }> } {
return typeof service === 'object' && service !== null && typeof (service as any).transform === 'function';
} Prevention
- Pin image.service.entrypoint explicitly in config so adapters cannot surprise you.
- Run a post-build SSR smoke request against one optimized image URL before shipping.
- Wrap adapter image handlers in an error-reporting middleware since this throw is unhandled.
When it happens
Trigger: Running SSR/preview behind an adapter that uses `handleImageRequest` while `image.service` resolves to a service lacking `transform` — e.g. `astro/assets/services/passthrough` or a custom service implementing only `getUrl` — and a page requests an optimized image URL.
Common situations: Adapters (node, edge, etc.) or integrations overriding `image.service` with a passthrough service; projects that disabled sharp in CI and then ran `astro preview` or SSR with `<Image>` components.
Related errors
- Configured image service is not a local service
- AdapterSupportOutputMismatch
- ⚠️ Astro expected an SVG for
- Failed to get static images from the Cloudflare prerender…
- GetImageNotUsedOnServer
AI-assisted analysis of withastro/astro@e294953aa8 (2026-08-18).
Data as JSON: /api/errors/cf8df9c57707f344.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/assets/endpoint/shared.ts:48
return Buffer.from(await res.arrayBuffer());
} catch {
return undefined;
}
}
export const handleImageRequest = async ({
request,
loadLocalImage,
logger,
}: {
request: Request;
loadLocalImage: (src: string, baseUrl: URL) => Promise<Buffer | undefined>;
logger: AstroRuntimeLogger;
}) => {
const imageService = await getConfiguredImageService();
if (!('transform' in imageService)) {
throw new Error('Configured image service is not a local service');
}
const url = new URL(request.url);
const transform = await imageService.parseURL(url, imageConfig, logger);
if (!transform?.src) {
return new Response('Invalid request', { status: 400 });
}
// Reject requests that attempt to convert a non-SVG source to SVG output.
// This mirrors the same guard in verifyOptions() that protects the <Image> component path.
if (transform.format === 'svg') {
const sourceFormat = inferSourceFormat(transform.src);
if (sourceFormat !== 'svg') {
return new Response('Cannot convert non-SVG source to SVG format', { status: 403 });
}
}
View on GitHub (pinned to e294953aa8)