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
Thrown by the `/_image` endpoint that serves optimized images in dev and SSR (packages/astro/src/assets/endpoint/generic.ts:19). The endpoint gets the configured image service via `getConfiguredImageService()` and requires it to implement `transform` — i.e. a local/base service that actually processes image bytes. Services that only rewrite URLs (passthrough, CDN-style, adapter-injected) cannot serve this endpoint, so any request to it fails and the endpoint returns a 500 after logging 'Could not process image request:'.
Solutions
- In `astro.config.mjs`, set `image.service.entrypoint` to a local service that implements `transform` (e.g. `astro/assets/services/sharp`, the default) and restart the dev server.
- If you intentionally use a CDN-style service (only `getUrl`), keep it for production but point dev/SSR at a local service via environment-specific config so the `/_image` endpoint works.
- Check whether an adapter or integration is overriding `image.service` with a passthrough service before your config takes effect.
- If you never want optimization, import images directly (plain `<img>` with an ESM-imported URL) so `/_image` is never called.
Example fix
// astro.config.mjs — before
image: { service: { entrypoint: 'astro/assets/services/passthrough' } }
// astro.config.mjs — after
image: { service: { entrypoint: 'astro/assets/services/sharp' } } Defensive patterns
Strategy: validation
Validate before calling
// astro.config.mjs — fail fast at config load if the service cannot transform
const serviceEntrypoint = 'astro/assets/services/sharp';
if (process.env.NODE_ENV === 'development' && !/sharp|base/.test(serviceEntrypoint)) {
throw new Error('Dev requires a local image service (implements transform), got: ' + serviceEntrypoint);
} Type guard
// For custom image service authors: guarantee a local service shape
import { baseService } from 'astro/assets/services/base';
export function isLocalService(service: unknown): service is typeof baseService {
return typeof service === 'object' && service !== null && 'transform' in service;
}
// build custom services on top of baseService so transform/parseURL always exist
export const service = { ...baseService /* , overrides */ }; Prevention
- Treat the passthrough service as production-only; never run dev/SSR image URLs against it.
- Assert the resolved image config after adapter setup (log it once in a config hook) so silent overrides surface early.
- Smoke-test one `<Image>` render in dev before pushing config changes.
When it happens
Trigger: A request reaches `/_image?...` while `image.service.entrypoint` resolves to a service without a `transform` method: `astro/assets/services/passthrough`, a custom/third-party service that only implements `getUrl`, or an adapter that swaps in a non-local service at runtime.
Common situations: Setting the passthrough service to avoid installing sharp, then rendering `<Image>`/`getImage()` which generate `/_image` URLs; edge adapters defaulting to a passthrough service; upgrading a project where the image service config changed.
Related errors
- Configured image service is not a local service
- AdapterSupportOutputMismatch
- Astro couldn't find the correct page to render, probably…
- Astro couldn't find the correct page to render, probably…
- ⚠️ Astro expected an SVG for
AI-assisted analysis of withastro/astro@e294953aa8 (2026-08-18).
Data as JSON: /api/errors/4144b665689eea8f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/assets/endpoint/generic.ts:19
// @ts-expect-error
import { imageConfig } from 'astro:assets';
import { isRemotePath } from '@astrojs/internal-helpers/path';
import { isRemoteAllowed } from '@astrojs/internal-helpers/remote';
import * as mime from 'mrmime';
import type { APIRoute } from '../../types/public/common.js';
import { getConfiguredImageService } from '../internal.js';
import { etag } from '../utils/etag.js';
import { loadImage } from './loadImage.js';
/**
* Endpoint used in dev and SSR to serve optimized images by the base image services
*/
export const GET: APIRoute = async ({ request, logger }) => {
try {
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) {
throw new Error('Incorrect transform returned by `parseURL`');
}
let inputBuffer: ArrayBuffer | undefined = undefined;
const isRemoteImage = isRemotePath(transform.src);
if (isRemoteImage && isRemoteAllowed(transform.src, imageConfig) === false) {
return new Response('Forbidden', { status: 403 });
}
const sourceUrl = new URL(transform.src, url.origin);View on GitHub (pinned to e294953aa8)