appwrite/appwrite · error · Appwrite\Extend\Exception
avatar_remote_url_failed
avatar_remote_url_failed
Error message
Failed to fetch favicon from the requested URL.
What it means
First SSRF guard in GET /v1/avatars/screenshots: the URL host is checked with `Utopia\Domains\Domain::isKnown()` (skipped only for IP-literal hosts) and an unknown domain throws avatar_remote_url_failed (whose default message is the shared avatars text 'Failed to fetch favicon...'). 'Known' means the registrable domain resolves against the bundled public-suffix data — hostnames with an unrecognized or missing TLD, or raw internal names, fail here.
Source
Thrown at src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php:105
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg', enum: new Enum(name: 'ImageFormat'))
->inject('response')
->inject('usage')
->callback($this->action(...));
}
public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, Context $usage)
{
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$host = \parse_url($url, PHP_URL_HOST) ?? '';
$isIpLiteral = \filter_var(\trim($host, '[]'), FILTER_VALIDATE_IP) !== false;
if (!$isIpLiteral) {
$domain = new Domain($host);
if (!$domain->isKnown()) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
}
$hostnameValidator = new PublicHostname();
if (!$hostnameValidator->isValid($host)) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, $hostnameValidator->getDescription());
}
$client = new Client();
$client->setTimeout(30 * 1000); // 30 seconds
$client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
// Convert indexed array to empty array (should not happen due to Assoc validator)
if (count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) {
$headers = [];
}
// Create a new object to ensure proper JSON serializationView on GitHub (pinned to feb9831e60)
Solutions
- Use a URL whose host has a real, public TLD (https://staging.example.com instead of http://staging).
- If you must screenshot an internal app, expose it under a real domain or use an IP-literal host (IP literals skip the isKnown check but still face the PublicHostname check).
- Keep the utopia-php/domains package updated so newly added TLDs are recognized.
Example fix
// before
await avatars.getScreenshot('http://intranet-portal:3000');
// after
await avatars.getScreenshot('https://portal.example.com'); Defensive patterns
Strategy: validation
Validate before calling
function hasKnownTld(host: string): boolean {
const parts = host.toLowerCase().split('.');
return parts.length >= 2 && parts[parts.length - 1].length >= 2;
} Try / catch
try { await avatars.getScreenshot(url); } catch (e) { if (e.code === 'avatar_remote_url_failed') { /* recheck host/TLD, fall back to public domain */ } } Prevention
- Only screenshot URLs under real public TLDs
- Expose internal apps via tunnels under your real domain
- Keep the utopia domains data fresh via upgrades
When it happens
Trigger: GET /v1/avatars/screenshots?url=http://my-intranet-app:3000 or ?url=https://example.invalid — any hostname whose TLD is not in the public suffix list, or a bare hostname like `http://localapp` without a dot/TLD.
Common situations: Trying to screenshot staging servers on internal hostnames (e.g. http://staging, .local, .internal); typo'd TLDs (.in instead of .in.); test domains on non-existent TLDs; fresh Utopia Domains data missing a newly introduced TLD.
Related errors
AI-assisted analysis of appwrite/appwrite@feb9831e60 (2026-08-18).
Data as JSON: /api/errors/044adc72bf6cbb9c.
Report an issue: GitHub.