{"record":{"id":"5b0f749373b54cdd","repo":"BookStackApp/BookStack","slug":"errors-cannot-get-image-from-url","errorCode":null,"errorMessage":"errors.cannot_get_image_from_url","messagePattern":"errors\\.cannot_get_image_from_url","errorType":"exception","errorClass":"HttpFetchException","httpStatus":null,"severity":"error","filePath":"app/Uploads/UserAvatars.php","lineNumber":158,"sourceCode":"    protected function getAvatarImageData(string $url): string\n    {\n        try {\n            $client = $this->http->buildClient(5);\n            $responseCount = 0;\n\n            do {\n                $response = $client->sendRequest(new Request('GET', $url));\n                $responseCount++;\n                $isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302);\n                $url = $response->getHeader('Location')[0] ?? '';\n            } while ($responseCount < 3 && $isRedirect && str_starts_with($url, 'http'));\n\n            if ($responseCount === 3) {\n                throw new HttpFetchException(\"Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}\");\n            }\n\n            if ($response->getStatusCode() !== 200) {\n                throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));\n            }\n\n            return (string) $response->getBody();\n        } catch (ClientExceptionInterface $exception) {\n            throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);\n        }\n    }\n\n    /**\n     * Check if fetching external avatars is enabled.\n     */\n    public function avatarFetchEnabled(): bool\n    {\n        $fetchUrl = $this->getAvatarUrl();\n\n        return str_starts_with($fetchUrl, 'http');\n    }\n","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Uploads/UserAvatars.php#L140-L176","documentation":"BookStack's UserAvatars::getAvatarImageData() fetches an external image via an HTTP client and throws HttpFetchException carrying the translated message errors.cannot_get_image_from_url when the response status is not 200. It is a guard against writing non-image/garbage responses (redirects, 403/404 pages, rate-limit blocks) into user avatar storage. The URL is included in the message so the failing remote source can be identified.","triggerScenarios":"Calling UserAvatars::assignToUserFromUrl or saveAvatarImage with a URL whose server replies 404/403/401/429/500 etc. (anything !== 200). Notably NOT thrown for >3 redirects (that throws a distinct max-redirect message).","commonSituations":"Hotlinking-protection returning 403 for non-browser user agents; typo'd or deleted image URLs; expired signed URLs (S3 presigned links); URLs requiring auth cookies; remote hosts rate-limiting BookStack's server-side fetch.","solutions":["Open the URL in the message from a server-side context (curl) and confirm it returns 200 with image content-type; fix or replace the URL","Check for hotlink protection / User-Agent blocking on the remote host and use a CDN or locally-uploaded avatar instead","Verify outbound HTTP (proxy/firewall) allows the request and no middleware rewrites the URL to an error page","Catch HttpFetchException in calling code and surface a friendly message pointing at the avatar URL"],"exampleFix":"// before\n$userAvatars->assignToUserFromUrl($user, 'https://example.com/avatar.png');\n// after\ntry {\n    $userAvatars->assignToUserFromUrl($user, $verifiedUrl);\n} catch (HttpFetchException $e) {\n    // pre-validate or fall back to default avatar\n}","handlingStrategy":"try-catch","validationCode":"// Validate URL is fetchable and returns an image before assigning\n$resp = \\Illuminate\\Support\\Facades\\Http::timeout(5)->get($url);\nif ($resp->failed() || !str_starts_with($resp->header('Content-Type') ?? '', 'image/')) {\n    throw new \\InvalidArgumentException(\"Avatar URL not fetchable: {$url}\");\n}","typeGuard":"function isHttpOkResponse(?\\Psr\\Http\\Message\\ResponseInterface $r): bool {\n    return $r !== null && $r->getStatusCode() === 200;\n}","tryCatchPattern":"try {\n    $avatars->assignToUserFromUrl($user, $url);\n} catch (\\BookStack\\Exceptions\\HttpFetchException $e) {\n    Log::warning('Avatar fetch failed', ['url' => $url, 'msg' => $e->getMessage()]);\n    // fall back to default avatar\n}","preventionTips":["Curl the URL server-side before storing it as an avatar source","Prefer locally uploaded avatars over remote hotlinked images","Check remote hotlink/UA-blocking policies","Set sensible timeouts and monitor outbound fetch failures"],"tags":["php","http","avatars","bookstack"],"backgroundTag":"http-non-200-response","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}