toeverything/AFFiNE · error · UnsplashIsNotConfigured

unsplash_is_not_configured

unsplash_is_not_configured

Error message

Unsplash is not configured.

What it means

Raised by the copilot controller's GET /unsplash/photos endpoint when the deployment has no Unsplash API key configured. The route proxies Unsplash's photo search for AI features; with copilot.unsplash.key empty (its default), the proxy refuses to forward the request and throws UnsplashIsNotConfigured so the client can show a proper 'not set up' state instead of an upstream 401.

Source

Thrown at packages/backend/server/src/plugins/copilot/controller.ts:314

      );

      return this.mergePingStream(prepared.messageId || '', source$);
    } catch (err) {
      metrics.ai.counter('images_stream_errors').add(1, info);
      return mapSseError(err, info);
    }
  }

  @Get('/unsplash/photos')
  @CallMetric('ai', 'unsplash')
  async unsplashPhotos(
    @Req() req: Request,
    @Res() res: Response,
    @Query() params: Record<string, string>
  ) {
    const { key } = this.config.copilot.unsplash;
    if (!key) {
      throw new UnsplashIsNotConfigured();
    }

    const query = new URLSearchParams(params);
    const response = await fetch(
      `https://api.unsplash.com/search/photos?${query}`,
      {
        headers: { Authorization: `Client-ID ${key}` },
        signal: getSignal(req).signal,
      }
    );

    res.set({
      'Content-Type': response.headers.get('Content-Type'),
      'Content-Length': response.headers.get('Content-Length'),
      'X-Ratelimit-Limit': response.headers.get('X-Ratelimit-Limit'),
      'X-Ratelimit-Remaining': response.headers.get('X-Ratelimit-Remaining'),
    });

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Set the copilot.unsplash.key config item to a valid Unsplash Access Key (Client-ID) and restart/reload the server.
  2. Verify the key is non-empty in the environment the request actually hits (config dump or diagnostics).
  3. On the client, check the advertised copilot capabilities before showing Unsplash search, and render 'not configured' when absent.
  4. If you do not want the feature, stop calling the endpoint rather than relying on the error.

Example fix

# before
# .env
AFFINE_COPILOT_UNSPLASH_KEY=

# after
AFFINE_COPILOT_UNSPLASH_KEY=your_unsplash_access_key
Defensive patterns

Strategy: validation

Validate before calling

if (!config.copilot.unsplash.key) {
  disableUnsplashSearchUi(); // don't call the endpoint unconfigured
}

Type guard

const isUnsplashConfigured = (config: Config): boolean =>
  Boolean(config.copilot?.unsplash?.key);

Try / catch

try {
  const photos = await fetch('/api/copilot/unsplash/photos?query=' + q);
} catch (e) {
  if (e?.code === 'unsplash_is_not_configured') {
    showNotice('Image search is not configured on this server');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling GET /api/copilot/unsplash/photos on any deployment where the copilot.unsplash.key config item was never filled (default key: ''); key set in the wrong environment (staging var set, production empty); config reload not applied after setting the key; key string containing only whitespace.

Common situations: Fresh self-hosted installs enabling copilot but skipping the Unsplash integration step; cloud environments where only some regions configure the key; frontends not gating the image-search UI on availability.

Understand the failure class

Background: "API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries — this error's family across 16 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/62883ce5b6f1bb7b. Report an issue: GitHub.