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

  1. 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.
  2. 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.
  3. Check whether an adapter or integration is overriding `image.service` with a passthrough service before your config takes effect.
  4. 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

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


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)