-
Notifications
You must be signed in to change notification settings - Fork 385
feat: Add Cloudflare Browser Rendering Plugin #1015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,4 @@ | ||
| import type { | ||
| AuthTypes, | ||
| BindEndpoints, | ||
| CorsairEndpoint, | ||
| CorsairErrorHandler, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import type { CloudflareBrowserRenderingContext } from './index'; | ||
|
|
||
| export class CloudflareBrowserRenderingClient { | ||
| private ctx: CloudflareBrowserRenderingContext; | ||
| private baseUrl = 'https://api.cloudflare.com/client/v4'; | ||
|
|
||
| constructor(ctx: CloudflareBrowserRenderingContext) { | ||
| this.ctx = ctx; | ||
| } | ||
|
|
||
| private async request<T>(path: string, options?: RequestInit): Promise<T> { | ||
| const apiKey = await this.ctx.keyBuilder('endpoint'); | ||
|
|
||
| const response = await fetch(`${this.baseUrl}${path}`, { | ||
| ...options, | ||
| headers: { | ||
| Authorization: `Bearer ${apiKey}`, | ||
| 'Content-Type': 'application/json', | ||
| ...options?.headers, | ||
| }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Cloudflare API error: ${response.status} ${response.statusText}`, | ||
| ); | ||
| } | ||
|
|
||
| const data = (await response.json()) as { | ||
| success: boolean; | ||
| result: T; | ||
| errors?: unknown[]; | ||
| }; | ||
|
|
||
| if (!data.success) { | ||
| throw new Error( | ||
| `Cloudflare API returned errors: ${JSON.stringify(data.errors)}`, | ||
| ); | ||
| } | ||
|
|
||
| return data.result; | ||
| } | ||
|
|
||
| async listAccounts() { | ||
| return this.request<Array<{ id: string; name: string }>>('/accounts'); | ||
| } | ||
|
|
||
| async captureScreenshot( | ||
| accountId: string, | ||
| url: string, | ||
| viewport?: { width: number; height: number }, | ||
| ) { | ||
| return this.request<string>( | ||
| `/accounts/${accountId}/browser-rendering/screenshot`, | ||
| { | ||
| method: 'POST', | ||
| body: JSON.stringify({ url, viewport }), | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| async takeWebpageSnapshot(accountId: string, url: string) { | ||
| return this.request<{ html: string; screenshot: string }>( | ||
| `/accounts/${accountId}/browser-rendering/snapshot`, | ||
| { | ||
| method: 'POST', | ||
| body: JSON.stringify({ url }), | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| async scrapeHtmlElements( | ||
| accountId: string, | ||
| url: string, | ||
| selectors: string[], | ||
| ) { | ||
| return this.request<Record<string, unknown>>( | ||
| `/accounts/${accountId}/browser-rendering/scrape`, | ||
| { | ||
| method: 'POST', | ||
| body: JSON.stringify({ | ||
| url, | ||
| elements: selectors.map((s) => ({ selector: s })), | ||
| }), | ||
| }, | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { CloudflareBrowserRenderingClient } from '../client'; | ||
| import type { CloudflareBrowserRenderingContext } from '../index'; | ||
| import type { | ||
| CaptureScreenshotCreateInput, | ||
| CaptureScreenshotCreateResponse, | ||
| } from './types'; | ||
|
|
||
| export const CaptureScreenshot = { | ||
| create: async ( | ||
| ctx: CloudflareBrowserRenderingContext, | ||
| input: CaptureScreenshotCreateInput, | ||
| ): Promise<CaptureScreenshotCreateResponse> => { | ||
| const client = new CloudflareBrowserRenderingClient(ctx); | ||
| const viewport = | ||
| input.viewportWidth && input.viewportHeight | ||
| ? { width: input.viewportWidth, height: input.viewportHeight } | ||
| : undefined; | ||
|
Comment on lines
+14
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject partial viewport input. If a caller sets only 🤖 Prompt for AI Agents |
||
| return client.captureScreenshot(input.accountId, input.url, viewport); | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { CaptureScreenshot } from './capture-screenshot'; | ||
| export { ListAccounts } from './list-accounts'; | ||
| export { ScrapeHtmlElements } from './scrape-html-elements'; | ||
| export { TakeWebpageSnapshot } from './take-webpage-snapshot'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { CloudflareBrowserRenderingClient } from '../client'; | ||
| import type { CloudflareBrowserRenderingContext } from '../index'; | ||
| import type { ListAccountsGetResponse } from './types'; | ||
|
|
||
| export const ListAccounts = { | ||
| get: async ( | ||
| ctx: CloudflareBrowserRenderingContext, | ||
| ): Promise<ListAccountsGetResponse> => { | ||
| const client = new CloudflareBrowserRenderingClient(ctx); | ||
| return client.listAccounts(); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { CloudflareBrowserRenderingClient } from '../client'; | ||
| import type { CloudflareBrowserRenderingContext } from '../index'; | ||
| import type { | ||
| ScrapeHtmlElementsCreateInput, | ||
| ScrapeHtmlElementsCreateResponse, | ||
| } from './types'; | ||
|
|
||
| export const ScrapeHtmlElements = { | ||
| create: async ( | ||
| ctx: CloudflareBrowserRenderingContext, | ||
| input: ScrapeHtmlElementsCreateInput, | ||
| ): Promise<ScrapeHtmlElementsCreateResponse> => { | ||
| const client = new CloudflareBrowserRenderingClient(ctx); | ||
| return client.scrapeHtmlElements( | ||
| input.accountId, | ||
| input.url, | ||
| input.selectors, | ||
| ); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { CloudflareBrowserRenderingClient } from '../client'; | ||
| import type { CloudflareBrowserRenderingContext } from '../index'; | ||
| import type { | ||
| TakeWebpageSnapshotCreateInput, | ||
| TakeWebpageSnapshotCreateResponse, | ||
| } from './types'; | ||
|
|
||
| export const TakeWebpageSnapshot = { | ||
| create: async ( | ||
| ctx: CloudflareBrowserRenderingContext, | ||
| input: TakeWebpageSnapshotCreateInput, | ||
| ): Promise<TakeWebpageSnapshotCreateResponse> => { | ||
| const client = new CloudflareBrowserRenderingClient(ctx); | ||
| return client.takeWebpageSnapshot(input.accountId, input.url); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { z } from 'corsair/core'; | ||
|
|
||
| // --- INPUT SCHEMAS --- | ||
|
|
||
| export const ListAccountsGetInputSchema = z.object({}).strict(); | ||
| export type ListAccountsGetInput = z.infer<typeof ListAccountsGetInputSchema>; | ||
|
|
||
| export const CaptureScreenshotCreateInputSchema = z | ||
| .object({ | ||
| accountId: z.string().describe('The Cloudflare account ID'), | ||
| url: z.string().url().describe('The URL to capture'), | ||
| viewportWidth: z.number().optional().describe('Viewport width in pixels'), | ||
| viewportHeight: z.number().optional().describe('Viewport height in pixels'), | ||
| }) | ||
| .strict(); | ||
| export type CaptureScreenshotCreateInput = z.infer< | ||
| typeof CaptureScreenshotCreateInputSchema | ||
| >; | ||
|
|
||
| export const TakeWebpageSnapshotCreateInputSchema = z | ||
| .object({ | ||
| accountId: z.string().describe('The Cloudflare account ID'), | ||
| url: z.string().url().describe('The URL to snapshot'), | ||
| }) | ||
| .strict(); | ||
| export type TakeWebpageSnapshotCreateInput = z.infer< | ||
| typeof TakeWebpageSnapshotCreateInputSchema | ||
| >; | ||
|
|
||
| export const ScrapeHtmlElementsCreateInputSchema = z | ||
| .object({ | ||
| accountId: z.string().describe('The Cloudflare account ID'), | ||
| url: z.string().url().describe('The URL to scrape'), | ||
| selectors: z | ||
| .array(z.string()) | ||
| .describe( | ||
| 'Array of CSS selectors to scrape (e.g., ["h1", ".class-name"])', | ||
| ), | ||
| }) | ||
| .strict(); | ||
| export type ScrapeHtmlElementsCreateInput = z.infer< | ||
| typeof ScrapeHtmlElementsCreateInputSchema | ||
| >; | ||
|
|
||
| // --- OUTPUT SCHEMAS --- | ||
|
|
||
| export const AccountSchema = z.object({ | ||
| id: z.string(), | ||
| name: z.string(), | ||
| }); | ||
|
|
||
| export const ListAccountsGetOutputSchema = z.array(AccountSchema); | ||
| export type ListAccountsGetResponse = z.infer< | ||
| typeof ListAccountsGetOutputSchema | ||
| >; | ||
|
|
||
| export const CaptureScreenshotCreateOutputSchema = z | ||
| .string() | ||
| .describe('Base64 encoded screenshot image'); | ||
| export type CaptureScreenshotCreateResponse = z.infer< | ||
| typeof CaptureScreenshotCreateOutputSchema | ||
| >; | ||
|
|
||
| export const TakeWebpageSnapshotCreateOutputSchema = z.object({ | ||
| html: z.string().describe('Rendered HTML content of the page'), | ||
| screenshot: z.string().describe('Base64 encoded screenshot image'), | ||
| }); | ||
| export type TakeWebpageSnapshotCreateResponse = z.infer< | ||
| typeof TakeWebpageSnapshotCreateOutputSchema | ||
| >; | ||
| export const ScrapeHtmlElementsCreateOutputSchema = z | ||
| .record(z.string(), z.unknown()) | ||
| .describe('Scraped HTML element data from Cloudflare'); | ||
| export type ScrapeHtmlElementsCreateResponse = z.infer< | ||
| typeof ScrapeHtmlElementsCreateOutputSchema | ||
| >; | ||
|
Comment on lines
+57
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- package files ---'
git ls-files packages/cloudflarebrowserrendering
printf '%s\n' '--- endpoint types ---'
cat -n packages/cloudflarebrowserrendering/endpoints/types.ts
printf '%s\n' '--- client ---'
cat -n packages/cloudflarebrowserrendering/client.ts
printf '%s\n' '--- endpoint implementations and tests ---'
rg -n -C 4 'captureScreenshot|takeWebpageSnapshot|scrapeHtmlElements|CaptureScreenshotCreateOutputSchema|TakeWebpageSnapshotCreateOutputSchema|ScrapeHtmlElementsCreateOutputSchema' packages/cloudflarebrowserrendering
printf '%s\n' '--- plugin rules ---'
cat .github/PLUGIN_PR_RULES.mdRepository: corsairdev/corsair Length of output: 25448 🌐 Web query:
💡 Result: The Cloudflare Browser Rendering API provides several endpoints for interacting with web pages, with distinct response formats for screenshots and data scraping [1][2]. Screenshot Endpoints The Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- endpoint wrappers ---'
for f in packages/cloudflarebrowserrendering/endpoints/*.ts; do
printf '\n### %s\n' "$f"
cat -n "$f"
done
printf '%s\n' '--- error handling and tests ---'
cat -n packages/cloudflarebrowserrendering/error-handlers.ts
cat -n packages/cloudflarebrowserrendering/schema.test.ts
printf '%s\n' '--- package metadata ---'
cat packages/cloudflarebrowserrendering/package.jsonRepository: corsairdev/corsair Length of output: 11230 🌐 Web query:
💡 Result: There is no standard HTTP Content-Type header for base64-encoded data. Base64 is an encoding mechanism, not a media type. When transmitting binary data encoded as base64 within an HTTP API response, the Content-Type should typically be set to application/json if the data is nested within a JSON object, or to the specific media type of the original binary content (e.g., image/png) if the response body consists solely of the encoded string. When designing an API response that includes a success result along with base64-encoded data, a common pattern is to wrap the data in a JSON structure that includes a success indicator. An example of such a response structure is: { "success": true, "data": "SGVsbG8gd29ybGQ=" } In this implementation, the Content-Type header should be set to application/json. If the intended use is to return only the binary content as base64 without JSON wrapping, you may use text/plain or a custom media type, though this is less standard for binary transport. To serve binary data natively, it is generally recommended to transmit the raw bytes with the appropriate media type (e.g., application/octet-stream or image/jpeg) and allow the client to handle the decoding, rather than forcing a base64 encoding layer on the HTTP transport. Citations: 🌐 Web query:
💡 Result: When working with technical workflows that require response content and optional screenshots, such as those provided by the Cloudflare Browser Rendering API, you can request multiple output formats in a single request [1][2]. The Cloudflare Browser Rendering Snapshot API allows you to retrieve the following optional formats as part of its result object [1][3]: - Content: The HTML content of the page [3][2]. - Screenshot: A base64-encoded image of the page [3][2]. - Markdown: The rendered Markdown content [3][2]. - Accessibility Tree: An object representing the accessibility tree nodes [1][3]. To create a response that includes these elements, you can specify the desired formats in your API request [2]. The API also provides configuration options such as: - Screenshot Customization: You can control the appearance of the screenshot using viewport, fullPage, or clip options [1][2]. - Page Loading Control: You can manage how the page loads before capture using gotoOptions and various waitFor parameters [1][2]. Example usage typically involves a POST request to the snapshot endpoint, where you provide the URL or HTML content and specify the formats you wish to receive in the response [2]. Citations:
🌐 Web query:
💡 Result: The term response result array selector is not a standard programming paradigm or universal API design pattern. Instead, it refers to a specific data structure used in the Cloudflare Browser Rendering API when performing web scraping tasks [1][2]. In this context, the elements parameter accepts an array of objects, where each object contains a selector field (typically a CSS selector) used to identify specific elements on a webpage [1][2]. When the API returns the results of this request, the response contains a result field, which is an array of objects [1][2]. Each object in this result array maps the requested selector back to the actual data extracted from the element (such as its HTML content, attributes, and geometric properties like height, width, and position) [1][2]. Essentially, this structure allows developers to request multiple elements via CSS selectors in a single API call and receive a paired, array-based response that links each selector to its corresponding scraped result [1][2]. Citations:
🌐 Web query:
💡 Result: Cloudflare provides a Browser Rendering REST API that enables developers to programmatically perform browser-based tasks such as capturing screenshots, scraping elements, and extracting page content [1][2]. API Endpoints and Functionality: - Screenshot ( Citations:
🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fsSL 'https://developers.cloudflare.com/api/resources/browser_rendering/subresources/screenshot/methods/create/' -o "$tmp/screenshot.html"
curl -fsSL 'https://developers.cloudflare.com/api/resources/browser_rendering/subresources/snapshot/methods/create/' -o "$tmp/snapshot.html"
curl -fsSL 'https://developers.cloudflare.com/api/resources/browser_rendering/subresources/scrape/methods/create/' -o "$tmp/scrape.html"
for f in "$tmp"/*.html; do
printf '\n--- %s ---\n' "$f"
rg -o -i '.{0,180}(base64|binary|content-type|content|screenshot|result|selector|response).{0,260}' "$f" \
| sed -E 's/<[^>]+>/ /g' \
| tr -s ' ' \
| head -n 80
done
printf '\n--- Cloudflare API schema references ---\n'
curl -fsSL 'https://api.github.com/repos/cloudflare/api-schemas/contents' \
| jq -r '.[].name' | head -n 80Repository: corsairdev/corsair Length of output: 29112 🌐 Web query:
💡 Result: The Cloudflare Browser Rendering API provides two primary ways to retrieve screenshots, depending on the endpoint used [1][2]. 1. /screenshot Endpoint: When using the POST /accounts/{account_id}/browser-rendering/screenshot endpoint, the API returns the screenshot directly as a raw binary image (image/png by default) [3][2]. This is the standard behavior when performing a direct request (e.g., via curl) where the output is saved to a file [3][2]. 2. /snapshot Endpoint: When using the POST /accounts/{account_id}/browser-rendering/snapshot endpoint, the API returns a JSON response that includes the screenshot as a base64-encoded string within the result object [4][5][6]. Regarding the encoding option: The Browser Rendering API allows you to specify the encoding for your screenshot in the screenshotOptions of your request. You can explicitly set the "encoding" parameter to either "binary" or "base64" [1]. If you require the response to be base64-encoded in your JSON payload, you should ensure this parameter is configured accordingly in your API request [1]. Summary of Key Details: - Default format: The screenshot is returned as a binary image (image/png) by default when using the /screenshot endpoint [3][2]. - JSON base64 support: The /snapshot endpoint returns base64-encoded images within a JSON response by design [4][6]. - Configurable options: You can control the encoding (binary vs. base64) and the image type (png, jpeg, or webp) via the screenshotOptions object in your request [1][7]. Note that if you set the quality parameter, you must also specify a supported image type (like jpeg) to avoid a 400 error [3][2]. Citations:
🌐 Web query:
💡 Result: There is no standard or industry-wide relationship between browser rendering, screenshots, and an Citations:
🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/cloudflare-docs/production/src/content/docs/browser-rendering/rest-api/screenshot-endpoint.mdx' -o "$tmp/screenshot.mdx"
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/cloudflare-docs/production/src/content/docs/browser-rendering/rest-api/snapshot-endpoint.mdx' -o "$tmp/snapshot.mdx"
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/cloudflare-docs/production/src/content/docs/browser-rendering/rest-api/scrape-endpoint.mdx' -o "$tmp/scrape.mdx"
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json' -o "$tmp/openapi.json"
for f in "$tmp"/*.mdx; do
printf '\n--- %s ---\n' "$f"
rg -n -i -C 5 'encoding|base64|binary|content|screenshot|result|selector|response|curl|application/json|image/' "$f" | head -n 240
done
printf '\n--- API schema browser-rendering paths ---\n'
jq -r '
.paths
| to_entries[]
| select(.key | contains("browser-rendering"))
| .key as $path
| [$path, (.value | keys[])] | `@tsv`
' "$tmp/openapi.json" | head -n 160
printf '\n--- API schema response/request fragments ---\n'
jq '
.paths
| to_entries[]
| select(.key | contains("browser-rendering"))
| {
path: .key,
operations: (
.value
| to_entries[]
| select(.key | IN("post","get"))
| {
method: .key,
requestBody: .value.requestBody,
responses: .value.responses
}
)
}
' "$tmp/openapi.json" | head -n 1000Repository: corsairdev/corsair Length of output: 207 🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
printf '%s\n' '--- documentation repository browser-rendering files ---'
curl -fsSL 'https://api.github.com/repos/cloudflare/cloudflare-docs/git/trees/production?recursive=1' \
| jq -r '.tree[]?.path' \
| rg 'browser-rendering|browser_rendering' \
| rg 'screenshot|snapshot|scrape|openapi|rest-api' \
| head -n 120
printf '%s\n' '--- API schema browser-rendering paths ---'
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json' -o "$tmp/openapi.json"
jq -r '
.paths
| to_entries[]
| select(.key | test("browser[-_]rendering"))
| .key
' "$tmp/openapi.json" | head -n 160
printf '%s\n' '--- API schema matching operation fragments ---'
jq '
.paths
| to_entries[]
| select(.key | test("browser[-_]rendering"))
| {
path: .key,
operations: (
.value
| to_entries[]
| select(.key | IN("post","get"))
| {
method: .key,
parameters: .value.parameters,
requestBody: .value.requestBody,
responses: .value.responses
}
)
}
' "$tmp/openapi.json" | head -n 1200Repository: corsairdev/corsair Length of output: 40583 🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json' -o "$tmp"
jq '
def brief_schema:
{
type,
format,
nullable,
required,
properties: (
.properties
| with_entries(.value |= {
type,
format,
nullable,
required,
items: (.items | if . == null then null else {type, properties: (.properties | keys), required} end)
})
),
items: (.items | if . == null then null else {type, properties: (.properties | keys), required} end)
};
.paths
| to_entries[]
| select(
.key == "/accounts/{account_id}/browser-rendering/screenshot"
or .key == "/accounts/{account_id}/browser-rendering/snapshot"
or .key == "/accounts/{account_id}/browser-rendering/scrape"
)
| {
path: .key,
post: {
request_properties: (
.value.post.requestBody.content["application/json"].schema.properties
| with_entries(.value |= {type, description, default, enum, required})
),
request_required: .value.post.requestBody.content["application/json"].schema.required,
response_200: (
.value.post.responses["200"].content
| to_entries
| map({
media_type: .key,
schema: (.value.schema | brief_schema)
})
)
}
}
' "$tmp"Repository: corsairdev/corsair Length of output: 7081 🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json' -o "$tmp"
python3 - "$tmp" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as f:
doc = json.load(f)
wanted = {
"/accounts/{account_id}/browser-rendering/screenshot",
"/accounts/{account_id}/browser-rendering/snapshot",
"/accounts/{account_id}/browser-rendering/scrape",
}
def schema_summary(schema):
if not isinstance(schema, dict):
return schema
out = {}
for key in ("$ref", "type", "format", "nullable", "required", "oneOf", "anyOf"):
if key in schema:
out[key] = schema[key]
if isinstance(schema.get("properties"), dict):
out["properties"] = {
name: schema_summary(value)
for name, value in schema["properties"].items()
}
if "items" in schema:
out["items"] = schema_summary(schema["items"])
return out
for path in sorted(wanted):
operation = doc["paths"][path]["post"]
request = operation.get("requestBody", {}).get("content", {}).get(
"application/json", {}
).get("schema", {})
print(f"\nPATH {path}")
print("REQUEST_REQUIRED", json.dumps(request.get("required", [])))
print("REQUEST_PROPERTIES", json.dumps(sorted(request.get("properties", {}))))
print("RESPONSES")
for status, response in operation.get("responses", {}).items():
content = response.get("content", {})
print(f" STATUS {status}")
for media_type, media in content.items():
print(f" MEDIA_TYPE {media_type}")
print(json.dumps(schema_summary(media.get("schema", {})), indent=2))
PYRepository: corsairdev/corsair Length of output: 12830 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- endpoint schema validation ---'
rg -n -C 5 'OutputSchema|output.*parse|parse.*output|RequiredPluginEndpointSchemas|EndpointOutput|safeParse' packages/corsair packages/cloudflarebrowserrendering \
-g '*.{ts,tsx,js,jsx}'
printf '%s\n' '--- exact snapshot formats schema ---'
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json' -o "$tmp"
python3 - "$tmp" <<'PY'
import json, sys
with open(sys.argv[1], encoding="utf-8") as f:
doc = json.load(f)
schema = doc["paths"]["/accounts/{account_id}/browser-rendering/snapshot"]["post"]["requestBody"]["content"]["application/json"]["schema"]
print(json.dumps(schema.get("properties", {}).get("formats"), indent=2))
PY
printf '%s\n' '--- plugin endpoint framework references ---'
rg -n -C 4 'CloudflareBrowserRenderingEndpointOutputSchemas|takeWebpageSnapshotCreate|scrapeHtmlElementsCreate|captureScreenshotCreate' packages/corsair packages/cloudflarebrowserrendering \
-g '*.{ts,tsx,js,jsx}'Repository: corsairdev/corsair Length of output: 22204 🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json' -o "$tmp"
python3 - "$tmp" <<'PY'
import json
from pathlib import Path
import sys
with open(sys.argv[1], encoding="utf-8") as f:
api = json.load(f)
client = Path("packages/cloudflarebrowserrendering/client.ts").read_text()
types = Path("packages/cloudflarebrowserrendering/endpoints/types.ts").read_text()
assert "response.json()" in client
assert "return data.result" in client
assert "request<string>" in client
assert "request<{ html: string; screenshot: string }>" in client
assert "request<Record<string, unknown>>" in client
assert ".string()" in types
assert "html: z.string()" in types
assert ".record(z.string(), z.unknown())" in types
paths = api["paths"]
screenshot = paths["/accounts/{account_id}/browser-rendering/screenshot"]["post"]
screenshot_media = set(
screenshot["responses"]["200"]["content"]
)
assert {"image/png", "image/jpg", "image/webp", "text/plain"} <= screenshot_media
assert "application/json" in screenshot_media
snapshot = paths["/accounts/{account_id}/browser-rendering/snapshot"]["post"]
snapshot_req = snapshot["requestBody"]["content"]["application/json"]["schema"]
assert snapshot_req["properties"]["formats"]["default"] == ["content", "screenshot"]
snapshot_result = (
snapshot["responses"]["200"]["content"]["application/json"]["schema"]
["properties"]["result"]
)
assert set(snapshot_result["properties"]) >= {"content", "screenshot"}
assert "html" not in snapshot_result["properties"]
assert "required" not in snapshot_result["properties"]
scrape = paths["/accounts/{account_id}/browser-rendering/scrape"]["post"]
scrape_result = (
scrape["responses"]["200"]["content"]["application/json"]["schema"]
["properties"]["result"]
)
assert scrape_result["type"] == "array"
assert set(scrape_result["items"]["properties"]) >= {"selector", "results"}
print("screenshot success media:", ", ".join(sorted(screenshot_media)))
print("snapshot default formats:", snapshot_req["properties"]["formats"]["default"])
print("snapshot result fields:", sorted(snapshot_result["properties"]))
print("scrape result type:", scrape_result["type"])
print("scrape item fields:", sorted(scrape_result["items"]["properties"]))
print("current client parser: response.json() -> data.result")
print("current public output shapes: string, {html, screenshot}, record")
PYRepository: corsairdev/corsair Length of output: 569 Align Cloudflare Browser Rendering response handling with the API contract.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| // --- EXPORTS FOR INDEX.TS --- | ||
|
|
||
| export const CloudflareBrowserRenderingEndpointInputSchemas = { | ||
| listAccountsGet: ListAccountsGetInputSchema, | ||
| captureScreenshotCreate: CaptureScreenshotCreateInputSchema, | ||
| takeWebpageSnapshotCreate: TakeWebpageSnapshotCreateInputSchema, | ||
| scrapeHtmlElementsCreate: ScrapeHtmlElementsCreateInputSchema, | ||
| } as const; | ||
|
|
||
| export const CloudflareBrowserRenderingEndpointOutputSchemas = { | ||
| listAccountsGet: ListAccountsGetOutputSchema, | ||
| captureScreenshotCreate: CaptureScreenshotCreateOutputSchema, | ||
| takeWebpageSnapshotCreate: TakeWebpageSnapshotCreateOutputSchema, | ||
| scrapeHtmlElementsCreate: ScrapeHtmlElementsCreateOutputSchema, | ||
| } as const; | ||
|
|
||
| export type CloudflareBrowserRenderingEndpointInputs = { | ||
| listAccountsGet: ListAccountsGetInput; | ||
| captureScreenshotCreate: CaptureScreenshotCreateInput; | ||
| takeWebpageSnapshotCreate: TakeWebpageSnapshotCreateInput; | ||
| scrapeHtmlElementsCreate: ScrapeHtmlElementsCreateInput; | ||
| }; | ||
|
|
||
| export type CloudflareBrowserRenderingEndpointOutputs = { | ||
| listAccountsGet: ListAccountsGetResponse; | ||
| captureScreenshotCreate: CaptureScreenshotCreateResponse; | ||
| takeWebpageSnapshotCreate: TakeWebpageSnapshotCreateResponse; | ||
| scrapeHtmlElementsCreate: ScrapeHtmlElementsCreateResponse; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { CorsairErrorHandler } from 'corsair/core'; | ||
| import { ApiError } from 'corsair/http'; | ||
|
|
||
| export const errorHandlers = { | ||
| RATE_LIMIT_ERROR: { | ||
| match: (error: Error) => { | ||
| if (error instanceof ApiError && error.status === 429) return true; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('rate_limited') || msg.includes('429'); | ||
| }, | ||
| handler: async (error: Error) => { | ||
| let retryAfterMs: number | undefined; | ||
| if (error instanceof ApiError && error.retryAfter !== undefined) { | ||
| retryAfterMs = error.retryAfter; | ||
| } | ||
| return { maxRetries: 5, headersRetryAfterMs: retryAfterMs }; | ||
| }, | ||
| }, | ||
| AUTH_ERROR: { | ||
| match: (error: Error) => { | ||
| if (error instanceof ApiError && error.status === 401) return true; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('unauthorized') || msg.includes('invalid_auth'); | ||
| }, | ||
| handler: async () => ({ maxRetries: 0 }), | ||
| }, | ||
| DEFAULT: { | ||
| match: () => true, | ||
| handler: async () => ({ maxRetries: 0 }), | ||
| }, | ||
| } satisfies CorsairErrorHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plugin gate evaluates the complete changeset, and this PR modifies files outside
packages/cloudflarebrowserrendering, the core provider registration, and the lockfile, causing the gate to reject the contribution. Remove the unrelated demo, existing-plugin, workspace, skills, and website changes or submit them separately.Rule Used: A plugin PR must only modify files inside a single... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!