Conversation
📝 WalkthroughWalkthroughAdds an avatars screenshot feature: new REST endpoint GET /v1/avatars/screenshots with extensive parameter validation, payload construction and remote browser service invocation, Imagick-based image processing and response handling; corresponding GraphQL query support; new config variable Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Security Scan Results for PRDocker Image Scan Results
Source Code Scan Results🎉 No vulnerabilities found! |
✨ Benchmark results
⚡ Benchmark Comparison
|
…arious header and parameter validations
…d GraphQL support and extensive tests for various scenarios
…for improved reliability
…v1/avatars/screenshots' for consistency; update related tests accordingly.
… scaling; update related tests to validate new functionality and edge cases.
…shot API by adding 'waitUntil' parameter and refactoring image processing logic for improved efficiency.
…eparate 'viewportWidth' and 'viewportHeight' parameters; update related tests for validation of new dimensions and edge cases.
…e communication; update avatar screenshot API to use System::getEnv for environment variable retrieval
Feat: usage stats for screenshot generated
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
tests/e2e/Services/Avatars/AvatarsBase.php (1)
575-579: Nice: body size assertion addedThis addresses the earlier feedback to assert a minimum payload size.
🧹 Nitpick comments (8)
app/config/variables.php (1)
955-964: Add basic URL validation filter for _APP_BROWSER_HOSTRecommend setting filter to 'url' to catch obvious misconfigurations early. Keeps behavior but adds guardrails.
Apply:
- 'filter' => '' + 'filter' => 'url'tests/e2e/Services/Avatars/AvatarsBase.php (3)
600-699: Avoid testing with sensitive headers (Authorization) or assert they’re blockedPassing Authorization to arbitrary sites can normalize risky patterns. Prefer benign headers (e.g., X-Test-Header) or add a negative test asserting security-sensitive headers (Host, Cookie, Authorization, X-Forwarded-*) are rejected/stripped by the service.
If the service blocks these, add:
// Expect 400 when sending disallowed headers $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ 'url' => 'https://appwrite.io', 'headers' => ['Host' => 'evil.test', 'Cookie' => 'a=b', 'Authorization' => 'Bearer X'], ]); $this->assertEquals(400, $response['headers']['status-code']);
728-741: Duplicate success case for max viewportThis repeats the earlier 1920x1080 success path (Lines 700–709). Consider removing one to shave test time.
877-889: Add positive cases for output and qualityYou test invalid output/quality but not valid combinations. Add a couple of happy paths to assert correct content-type/encoding.
Apply after this block:
+ // SUCCESS: output=jpg with quality + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'output' => 'jpg', + 'quality' => 80, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/jpeg', $response['headers']['content-type']); + + // SUCCESS: output=webp + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'output' => 'webp', + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/webp', $response['headers']['content-type']);tests/e2e/Services/GraphQL/AvatarsTest.php (2)
198-203: Remove noisy debug echos from passing path.Echoing headers/body on non-image responses adds noise to CI logs and can leak data. Prefer asserting and letting the failure output surface, or only print on assertion failure via PHPUnit messages.
Apply this diff:
- // Debug: Print the actual response if it's not an image - if (!str_contains($screenshot['headers']['content-type'], 'image/')) { - echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n"; - echo "Response body: " . print_r($screenshot['body'], true) . "\n"; - }Also applies to: 229-234, 281-286, 345-350
240-270: Exercise output/quality variants as well.Consider adding cases for output ("webp", "jpeg") and quality boundaries to cover format negotiation through GraphQL too.
app/controllers/api/avatars.php (2)
699-713: Over-complicated headers serialization; avoid stdClass and duplicate config building.json_encode will serialize assoc arrays as JSON objects; forcing stdClass and rebuilding $finalConfig is unnecessary and error-prone.
Apply this diff:
- // Create a new object to ensure proper JSON serialization - $headersObject = new \stdClass(); - foreach ($headers as $key => $value) { - $headersObject->$key = $value; - } - - // Create the config with headers as an object + // Build request config; assoc arrays serialize to JSON objects $config = [ 'url' => $url, 'theme' => $theme, - 'headers' => $headersObject, + 'headers' => $headers, 'sleep' => $sleep * 1000, // Convert seconds to milliseconds 'waitUntil' => 'load', 'viewport' => [ 'width' => $viewportWidth, 'height' => $viewportHeight ] ]; @@ - // Manually handle the config to ensure headers is an object but arrays remain arrays - $finalConfig = [ - 'url' => $config['url'], - 'theme' => $config['theme'], - 'headers' => $config['headers'], // Keep as object - 'sleep' => $config['sleep'], - 'viewport' => $config['viewport'] // Keep as object - ]; - // Add scale if not default - if ($scale != 1) { - $finalConfig['deviceScaleFactor'] = $scale; - } - // Add optional parameters that were set, preserving arrays as arrays - if (!empty($userAgent)) { - $finalConfig['userAgent'] = $userAgent; - } - if ($fullpage) { - $finalConfig['fullPage'] = true; - } - if (!empty($locale)) { - $finalConfig['locale'] = $locale; - } - if (!empty($timezone)) { - $finalConfig['timezoneId'] = $timezone; - } - // Add geolocation if any coordinates are provided - if ($latitude != 0 || $longitude != 0) { - $finalConfig['geolocation'] = [ - 'latitude' => $latitude, - 'longitude' => $longitude, - 'accuracy' => $accuracy - ]; - } - if ($touch) { - $finalConfig['hasTouch'] = true; - } - // Add permissions if provided (preserve as array) - if (!empty($permissions)) { - $finalConfig['permissions'] = $permissions; // Keep as array - } - $config = $finalConfig; + // Optional params + if ($scale != 1) $config['deviceScaleFactor'] = $scale; + if (!empty($userAgent)) $config['userAgent'] = $userAgent; + if ($fullpage) $config['fullPage'] = true; + if (!empty($locale)) $config['locale'] = $locale; + if (!empty($timezone)) $config['timezoneId'] = $timezone; + if ($latitude != 0 || $longitude != 0) { + $config['geolocation'] = ['latitude' => $latitude, 'longitude' => $longitude, 'accuracy' => $accuracy]; + } + if ($touch) $config['hasTouch'] = true; + if (!empty($permissions)) $config['permissions'] = $permissions;Also applies to: 773-823
843-860: Processing path and content-type are sound; minor polish.Logic correctly skips processing for original images and maps output to storage-outputs. Consider defaulting output based on actual screenshot mime if the service returns non-PNG (future-proofing).
Also applies to: 861-871
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
docs/references/avatars/get-screenshot.mdis excluded by!docs/references/**
📒 Files selected for processing (11)
app/config/variables.php(1 hunks)app/controllers/api/avatars.php(3 hunks)app/controllers/shared/api.php(3 hunks)app/init/constants.php(1 hunks)app/views/install/compose.phtml(1 hunks)docker-compose.yml(1 hunks)src/Appwrite/GraphQL/Types/Mapper.php(1 hunks)src/Appwrite/Platform/Modules/Functions/Workers/Builds.php(1 hunks)tests/e2e/Services/Avatars/AvatarsBase.php(1 hunks)tests/e2e/Services/GraphQL/AvatarsTest.php(1 hunks)tests/e2e/Services/GraphQL/Base.php(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
app/controllers/shared/api.php (1)
src/Appwrite/Event/StatsUsage.php (1)
disableMetric(61-66)
tests/e2e/Services/Avatars/AvatarsBase.php (3)
tests/e2e/Services/GraphQL/AvatarsTest.php (1)
testGetScreenshot(177-207)tests/e2e/Client.php (1)
Client(8-342)tests/e2e/Scopes/Scope.php (1)
getProject(150-150)
tests/e2e/Services/GraphQL/AvatarsTest.php (4)
tests/e2e/Services/Avatars/AvatarsBase.php (1)
testGetScreenshot(562-1295)tests/e2e/Services/GraphQL/Base.php (1)
getQuery(519-3448)tests/e2e/Client.php (1)
Client(8-342)tests/e2e/Scopes/Scope.php (1)
getHeaders(145-145)
app/controllers/api/avatars.php (3)
src/Appwrite/Utopia/Response.php (2)
Response(158-980)file(838-845)src/Appwrite/URL/URL.php (1)
URL(5-121)src/Appwrite/Event/StatsUsage.php (1)
addMetric(45-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: E2E Service Test (Databases/TablesDB)
🔇 Additional comments (13)
app/views/install/compose.phtml (1)
862-862: LGTM! Browser service version updated.The appwrite-browser image version bump from 0.2.4 to 0.3.1 aligns with the new screenshot functionality introduced in this PR.
docker-compose.yml (2)
464-464: Good addition of browser host environment variable.Adding
_APP_BROWSER_HOSTto the worker-builds service environment enables runtime configuration of the browser service endpoint, supporting the refactor insrc/Appwrite/Platform/Modules/Functions/Workers/Builds.php.
961-961: LGTM! Browser service version updated.Consistent with the production compose template change.
app/init/constants.php (1)
273-273: LGTM! New metric constant added.The constant
METRIC_AVATARS_SCREENSHOTS_GENERATEDfollows the established naming convention and is appropriately placed in the metrics section.app/controllers/shared/api.php (2)
57-62: Good defensive type handling for label substitution.Converting non-string replacements to strings (JSON-encoding arrays) ensures safe string substitution and prevents type errors during label parsing.
588-591: Correct metric handling for cached screenshots.Disabling
METRIC_AVATARS_SCREENSHOTS_GENERATEDon cache hits prevents double-counting and accurately reflects that screenshots are served from cache rather than newly generated.src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (1)
991-991: Good refactor to environment-based configuration.Moving the browser service endpoint from configuration to an environment variable with a sensible default enables runtime configuration while maintaining backward compatibility.
src/Appwrite/GraphQL/Types/Mapper.php (1)
336-343: Excellent enhancement for float range support.The refined Range validator mapping correctly distinguishes between float and integer types, enabling accurate GraphQL type representation for numeric parameters like dimensions and viewport values in the screenshot endpoint.
tests/e2e/Services/GraphQL/Base.php (2)
291-292: Constant addition looks goodNaming aligns with existing Avatars constants.
1785-1790: Parity check: GraphQL args vs REST; confirm intended omissionsREST supports headers, output, quality, sleep; this GraphQL query omits them. If intentional, fine; otherwise consider adding for parity. Also verify resolver signature matches these arg names/types.
Run to confirm schema/resolver args:
tests/e2e/Services/GraphQL/AvatarsTest.php (2)
356-385: Error contract check looks good.Asserting HTTP 200 with GraphQL errors array is consistent with GraphQL semantics. Keep this pattern for other invalid-params cases if added later.
If the server changes to use non-200 on GraphQL validation errors, update expected status accordingly.
195-205: GraphQL response type vs. assertions mismatch — please verify.These tests assert an image Content-Type from the GraphQL endpoint. However, the GET_SCREENSHOT GraphQL query (per tests/e2e/Services/GraphQL/Base.php excerpt) returns a field named status, which implies a JSON response. If the GraphQL layer streams binary for LOCATION methods, fine; otherwise these assertions will fail. Please confirm the actual GraphQL behavior for avatarsGetScreenshot and align assertions or query accordingly.
Run this to inspect the GET_SCREENSHOT query and confirm expected response type:
Also applies to: 235-236, 287-288, 351-352
app/controllers/api/avatars.php (1)
665-685: Validator set looks good.Assoc for headers and ArrayList+WhiteList for permissions are appropriate; good coverage on ranges and text lengths.
| $domain = new Domain(\parse_url($url, PHP_URL_HOST)); | ||
|
|
||
| if (!$domain->isKnown()) { | ||
| throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); | ||
| } | ||
|
|
||
| $client = new Client(); | ||
| $client->setTimeout(30); | ||
| $client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON); |
There was a problem hiding this comment.
SSRF hardening and timeout tuning.
- Domain::isKnown() is a good first gate but won’t block DNS rebinding or public-looking domains resolving to private IPs. Ensure the browser service itself enforces private-network blocking, IP allow/deny lists, and redirect validation.
- setTimeout(30) might be too low when sleep>0 and fullpage is true. Consider 60s or make it proportional.
Would you like a hardening checklist and example IP resolution guard for the browser service?
Also applies to: 824-836
🤖 Prompt for AI Agents
In app/controllers/api/avatars.php around lines 693-701 (and similarly at
824-836), the current code only uses Domain::isKnown() and sets a 30s client
timeout, which leaves SSRF and timeout issues; update to enforce SSRF hardening
by resolving the hostname before making requests and validating all resolved IPs
against allow/deny rules and private-network ranges (block RFC1918, link-local,
loopback, unique local, multicast, etc.) to prevent DNS rebinding and private IP
access, ensure redirects are validated (reject redirects to disallowed
IPs/ranges), and move IP validation logic to the browser service so it applies
consistently; increase the HTTP client timeout to 60s or make it proportional to
parameters like sleep/fullpage (e.g., baseTimeout * (sleep? factor:1)) and
surface errors with clear messages when validation fails.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
app/controllers/api/avatars.php (2)
700-700: Timeout may be insufficient for fullpage + sleep scenarios.The 30-second timeout can be exceeded when:
fullpage=trueon long pages requiring scroll and multiple renderssleepis set to 10 seconds (the maximum)- Page has slow-loading resources
Consider increasing to 60 seconds or making it proportional:
setTimeout(30 + $sleep + ($fullpage ? 20 : 0))
817-819: Generic exception handling loses error context.The catch block swallows all
Throwabletypes and converts them to a genericAVATAR_REMOTE_URL_FAILEDexception. This loses important context:
- Network timeouts vs. browser service errors vs. image processing failures
- HTTP status codes from the browser service
- Specific Imagick errors
Consider preserving more context:
} catch (\Throwable $th) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage()); + $message = 'Screenshot generation failed: ' . $th->getMessage(); + if ($th instanceof \ImagickException) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Image processing failed: ' . $th->getMessage()); + } + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, $message, $th->getCode(), $th); }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/controllers/api/avatars.php(3 hunks)app/controllers/shared/api.php(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/controllers/shared/api.php (1)
src/Appwrite/Event/StatsUsage.php (1)
disableMetric(61-66)
app/controllers/api/avatars.php (2)
src/Appwrite/Utopia/Response.php (2)
Response(158-980)file(838-845)src/Appwrite/Event/StatsUsage.php (1)
addMetric(45-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Setup & Build Appwrite Image
- GitHub Check: Setup & Build Appwrite Image
- GitHub Check: scan
🔇 Additional comments (6)
app/controllers/shared/api.php (1)
588-591: LGTM!The logic correctly disables the screenshot generation metric when serving from cache, preventing double-counting. This is the appropriate behavior for cache hits.
app/controllers/api/avatars.php (5)
665-684: LGTM!Parameter definitions are comprehensive and use appropriate validators. The viewport is correctly split into separate width/height parameters, and the permissions array validator properly constrains allowed values.
704-712: LGTM – defensive headers conversion.The indexed array check (lines 704-706) provides defense-in-depth even though the
Assocvalidator should prevent it. Converting tostdClass(lines 708-712) ensures headers serialize as a JSON object{}rather than an array[], which is correct for HTTP headers.
714-767: LGTM – config construction is well-structured.The configuration object is built correctly:
- Sleep time properly converted to milliseconds (line 720)
- Optional parameters conditionally added only when non-default
fullPageset at top level (line 739) without duplication in viewport- Arrays like
permissionspreserved as arrays (line 765)
768-786: LGTM – screenshot generation logic is sound.The browser service integration:
- Correctly uses
System::getEnv()for configuration (line 769)- Properly handles HTTP errors with status codes >= 400 (lines 777-779)
- Validates screenshot data before processing (lines 783-785)
- Provides helpful error messages including service responses
787-814: LGTM – image processing logic is efficient.The conditional processing (line 788) appropriately skips expensive operations when not needed. The implementation:
- Correctly crops/resizes only when dimensions specified (line 794)
- Applies quality settings properly (line 797)
- Sets appropriate Content-Type from config (lines 805-808)
- Updates metrics before response (line 809)
What does this PR do?
This pull request adds a new API endpoint to the
avatarsservice that allows users to generate and retrieve webpage screenshots with customizable options. It also introduces new validators to support the new endpoint's parameters.New Feature: Webpage Screenshot API
GET /v1/avatars/screenshotendpoint inapp/controllers/api/avatars.phpto capture screenshots of webpages, with support for parameters such asurl,headers,viewport,scale,fullPage,sleep,width,height,quality, andoutputformat. The endpoint validates inputs, interacts with a browser service, processes the image, and returns the screenshot in the requested format.Validator Enhancements
AnyOfandAssoc) to validate the new endpoint's parameters, ensuring flexible and robust input handling for HTTP headers and other options.<!--Thank you for sending the PR! We appreciate you spending the time to work on these changes.
Test Plan
(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)
Related PRs and Issues
Checklist