Skip to content

feat(vercel): add image optimization - #13

Open
RihanArfan wants to merge 3 commits into
mainfrom
feat/vercel-image
Open

feat(vercel): add image optimization#13
RihanArfan wants to merge 3 commits into
mainfrom
feat/vercel-image

Conversation

@RihanArfan

@RihanArfan RihanArfan commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

Adds Vercel image optimization route handling to the Vercel env-runner preset, using IPX for image processing.

Usage

import { VercelEnvRunner } from "env-runner/runners/vercel";

const runner = new VercelEnvRunner({
  name: "my-app",
  data: { entry: "./server.ts" },
  // Optional: pass the Build Output API `images` config
  images: {
    sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    domains: ["cdn.example.com"],
    remotePatterns: [
      { protocol: "https", hostname: "**.example.com", pathname: "/assets/**" },
    ],
    formats: ["image/webp", "image/avif"],
    minimumCacheTTL: 300,
  },
});

ipx is needed otherwise no image transformations are done.

npx nypm i -D ipx

Without images config, local paths are allowed with sensible defaults (format negotiation via Accept header, 60s cache TTL), but remote URLs are rejected, matching Vercel's default-deny behavior. Configure domains or remotePatterns to allow specific remote hosts.

The images option accepts the same shape as the images property in .vercel/output/config.json, so framework presets can forward it directly:

const runner = new VercelEnvRunner({
  name: "my-app",
  data: { entry: serverEntry },
  images: vercelOutputConfig.images, // straight from .vercel/output/config.json
});

Supported query parameters

Parameter Type Required Description
url string Yes Source image URL (local path or absolute URL)
w integer Yes Output width in pixels
q integer No Quality 1–100 (default: 75)

These are the only params the real /_vercel/image endpoint honors, and every other query param is ignored to match it, so there is deliberately no h/fit/blur/cache, and no format-pinning param either.

Config options (VercelImageConfig)

Matches the Build Output API ImagesConfig. Every field is optional, but omitting one does not always widen what is allowed: sizes, localPatterns, qualities and formats are unrestricted when unset, while domains/remotePatterns deny every remote source until configured, dangerouslyAllowSVG blocks SVG, and blockPrivateIPs defaults to true.

Option Type Description
sizes number[] Allowed output widths (rejects others with 400)
domains string[] Allowed remote hostnames (exact match)
remotePatterns RemotePattern[] Remote URL patterns with protocol, hostname, port, pathname, search
localPatterns LocalPattern[] Local URL patterns with pathname, search
qualities number[] Allowed quality values
formats string[] Allowed output formats
minimumCacheTTL number Cache duration in seconds (default: 60)
dangerouslyAllowSVG boolean Allow SVG sources (blocked by default)
contentSecurityPolicy string CSP header for responses
contentDispositionType string Content-Disposition header type
blockPrivateIPs boolean env-runner extension (not part of Vercel's config): reject remote sources that are, or resolve to, a non-public IP (default: true)

Pattern fields (hostname, pathname) accept both formats:

  • Build Output API regex: "^cdn\\.example\\.com$", "^/assets/.*$"
  • Glob patterns: "**.example.com", "/assets/**"

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added Vercel-compatible optimization for local and remote images.
    • Added configurable image allowlists, format negotiation, caching, validation, and security controls.
    • Added public APIs for creating and configuring image handlers.
    • Added fallback image proxying when optimization is unavailable.
  • Bug Fixes
    • Improved request readiness, error responses, header handling, and cache revalidation.
  • Documentation
    • Updated Vercel documentation with image optimization guidance and configuration details.

@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Vercel image optimization

Layer / File(s) Summary
Image handler and public API
src/runners/vercel/image.ts, src/index.ts, package.json, AGENTS.md, .agents/VERCEL.md
Adds the public image handler, request validation, IPX integration, fallback fetching, caching, security headers, exports, optional peer dependency metadata, and documentation.
Vercel runner integration
src/runners/vercel/runner.ts, test/fixtures/app-image.mjs, test/vercel.test.ts, .agents/VERCEL.md
Routes /_vercel/image requests through a cached handler, preserves request and deployment headers, handles readiness and unavailable runners, and closes image state.
Image handler validation and fallback tests
test/vercel-image.test.ts, .agents/VERCEL.md
Tests methods, parameters, allowlists, SVG handling, format negotiation, headers, caching, IPX failures, and unoptimized fallback behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • unjs/env-runner#3: Adds the Vercel runner that this change extends with image optimization.
  • unjs/env-runner#33: Adds runtime-aware proxy and readiness mechanisms reused by the Vercel runner integration.

Suggested reviewers: pi0

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant VercelEnvRunner
  participant VercelImageHandler
  participant IPX
  participant Worker
  Client->>VercelEnvRunner: Request /_vercel/image
  VercelEnvRunner->>VercelImageHandler: Forward image Request
  VercelImageHandler->>IPX: Process validated image modifiers
  IPX->>Worker: Fetch local image data
  Worker-->>IPX: Return image data
  IPX-->>VercelImageHandler: Return optimized response
  VercelImageHandler-->>VercelEnvRunner: Return image response
  VercelEnvRunner-->>Client: Return Vercel image response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding Vercel image optimization.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vercel-image

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
src/runners/vercel/image.ts (4)

33-46: Explicit return undefined in the catch branch.

Readability nit: the catch sets _ipxLoadResult = false and falls through to an implicit undefined return. Make the contract obvious and silence potential lint complaints:

♻️ Tweak
   } catch {
     _ipxLoadResult = false;
     console.warn(
       "ipx is not installed. Install it for Vercel image optimization: npx nypm i -D ipx",
     );
+    return undefined;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runners/vercel/image.ts` around lines 33 - 46, The catch branch of
loadIPX currently assigns _ipxLoadResult = false and relies on implicit
undefined return; make the contract explicit by adding an explicit return
undefined in the catch so callers and linters see the intended Promise<IPXModule
| undefined> outcome. Update the catch block inside function loadIPX to set
_ipxLoadResult = false, log the warning as before, then return undefined.

321-366: Minor: short-circuit SVG on explicit f=image/svg+xml before process().

If the URL-based SVG check is bypassed (e.g. source doesn’t have a .svg extension) and the caller sets f=image/svg+xml, modifiers.format becomes "svg+xml", IPX runs a full decode/encode, and only then is the response rejected at line 326. Cheap to short-circuit earlier right after the f parse:

♻️ Proposed tweak
       if (f && config?.formats?.length && !config.formats.includes(f)) {
         return new Response(`"f" must be one of: ${config.formats.join(", ")}`, { status: 400 });
       }
+      if (!config?.dangerouslyAllowSVG && f === "image/svg+xml") {
+        return new Response('"url" parameter is valid but image type is not allowed', { status: 400 });
+      }

Also, catch (error: any) at line 363 could be catch (error) with a unknown narrowing for type hygiene, though it’s not user-visible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runners/vercel/image.ts` around lines 321 - 366, Short-circuit SVG
handling by checking the parsed modifiers.format immediately after parsing the
request and before calling ipx/process(): if modifiers.format === "svg+xml" and
config?.dangerouslyAllowSVG is falsy, return the same 400 Response used for
blocked SVGs (matching the message at the later format check) to avoid running
ipx.process(); also change the catch clause from catch (error: any) to catch
(error) and narrow error (e.g., extract statusCode/message safely) for better
type hygiene. Ensure you reference the existing modifiers, ipx(sourceUrl,
modifiers), process(), format check and the Response construction so the
behavior and responses remain consistent.

62-82: matchPattern regex heuristic is fragile and exposes ReDoS from config.

Two concerns:

  1. Any pattern starting with ^ or ending with $ is interpreted as a regex (line 64). Glob patterns that happen to contain these characters would be silently misinterpreted. Consider a more explicit marker (e.g. only treat patterns that both start with ^ and end with $ as regex, which matches the Build Output API convention), or an explicit discriminator in the config type.
  2. new RegExp(pattern) on user-supplied input can trigger catastrophic backtracking (ast-grep flagged CWE-1333). The config originates from the developer (not end-users), so impact is bounded, but worth either documenting or guarding with a tested regex library (e.g. recheck) or a compile-time timeout.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runners/vercel/image.ts` around lines 62 - 82, The matchPattern function
currently treats any pattern starting with "^" OR ending with "$" as a regex and
passes user-supplied input straight to new RegExp, which mis-classifies globs
and can enable ReDoS; update matchPattern so it only treats a pattern as a regex
when it both startsWith("^") and endsWith("$") (or when an explicit
discriminator is present), and avoid direct unsafe RegExp compilation by
validating and constraining the input before compiling: enforce a sensible max
pattern length, sanitize/escape or reject obviously dangerous constructs, and
wrap RegExp construction/test in a try/catch (or switch to a safe regex library
like recheck) so that if compilation fails or is potentially unsafe you fall
back to the glob-to-regex path; reference the matchPattern function, the pattern
variable, and the sites where new RegExp(pattern) is called and replace those
usages accordingly.

175-222: Race on concurrent IPX initialization.

If two requests hit getIPX() before _ipx is assigned (e.g. two concurrent /_vercel/image requests on a cold runner), both will await loadIPX() and both will call ipxModule.createIPX(...), so the first assignment is overwritten and an extra IPX instance is created without cleanup. Memoize the pending promise:

♻️ Proposed fix
-  let _ipx: ReturnType<IPXModule["createIPX"]> | undefined;
+  let _ipx: ReturnType<IPXModule["createIPX"]> | undefined;
+  let _ipxPromise: Promise<ReturnType<IPXModule["createIPX"]> | undefined> | undefined;

   async function getIPX() {
     if (_ipx) return _ipx;
-    const ipxModule = await loadIPX();
-    ...
-    _ipx = ipxModule.createIPX({ ... });
-    return _ipx;
+    if (_ipxPromise) return _ipxPromise;
+    _ipxPromise = (async () => {
+      const ipxModule = await loadIPX();
+      if (!ipxModule) return undefined;
+      // ...workerStorage + createIPX...
+      _ipx = ipxModule.createIPX({ /* ... */ });
+      return _ipx;
+    })();
+    return _ipxPromise;
   }

Also reset _ipxPromise alongside _ipx in close().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runners/vercel/image.ts` around lines 175 - 222, getIPX has a race:
concurrent callers can each call loadIPX() and createIPX(), so memoize the
pending initialization by introducing a module-scoped _ipxPromise alongside
_ipx; in getIPX, if _ipx exists return it, else if _ipxPromise exists await it,
otherwise set _ipxPromise = (async () => { const ipxModule = await loadIPX(); if
(!ipxModule) return undefined; const instance = ipxModule.createIPX({...}); _ipx
= instance; _ipxPromise = undefined; return instance; })(); then await and
return the resolved instance instead of calling createIPX multiple times; also
update close() to clear both _ipx and _ipxPromise so subsequent initialization
can run cleanly (references: getIPX, loadIPX, _ipx, _ipxPromise, createIPX,
close).
src/runners/vercel/runner.ts (1)

85-102: Minor: forwarded image Request drops original method/body.

new Request(requestUrl, { headers }) on line 97 only carries the URL and the (already-merged) headers. If the inbound input was a Request with a non-GET method or a body, those are silently dropped. In practice /_vercel/image is always GET, but for consistency with the other branches (which preserve input/init), consider propagating method and body when applicable:

♻️ Proposed tweak
-      res = await this._imageHandler.handle(new Request(requestUrl, { headers }));
+      const imageReq =
+        input instanceof Request
+          ? new Request(requestUrl, { ...init, headers, method: input.method, body: input.body, duplex: "half" } as any)
+          : new Request(requestUrl, { ...init, headers });
+      res = await this._imageHandler.handle(imageReq);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runners/vercel/runner.ts` around lines 85 - 102, The image request branch
creates a new Request with only URL and headers which drops method/body from the
original input; update the logic around requestUrl handling so when constructing
the Request passed to this._imageHandler.handle you propagate the original
request semantics (if input is a Request use input.method, input.body and other
relevant properties; otherwise use init.method and init.body when present) and
merge headers, ensuring this._imageHandler and createVercelImageHandler usage
stays the same; locate symbols requestUrl, input, init, this._imageHandler, and
_imageHandler.handle to apply the change.
test/vercel.test.ts (1)

267-280: Remote-URL tests can hit real DNS and slow/flake CI.

Tests that expect “validation passed but fetch fails” (e.g. allowed.example.com, cdn.example.com, any.example.com, other.com) will cause IPX (or the fallback) to perform actual outbound fetch() calls. Depending on the network sandbox:

  • DNS resolution may hang until timeout (slow tests).
  • example.com/other.com actually resolve, so the request will connect and potentially return real bytes rather than fail.
  • In offline/airgapped CI, behavior is non-deterministic.

Prefer pointing at a guaranteed-unroutable target for deterministic failure, or assert on the specific validation behavior without relying on the downstream fetch to fail:

♻️ Example
-      images: { domains: ["allowed.example.com"] },
+      images: { domains: ["allowed.invalid"] },
 ...
-      "http://localhost/_vercel/image?url=https://allowed.example.com/img.png&w=100&q=75",
+      "http://localhost/_vercel/image?url=https://allowed.invalid/img.png&w=100&q=75",

.invalid is reserved by RFC 2606 and is guaranteed not to resolve.

Also applies to: 282-303, 305-339, 365-376

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/vercel.test.ts` around lines 267 - 280, Replace tests that rely on real
DNS/remote fetch failures by using an unroutable reserved TLD (e.g., hostnames
under .invalid) or by asserting validation behavior only; specifically update
VercelEnvRunner test cases (the "allows remote URL when domain matches" test and
the other tests referencing allowed.example.com, cdn.example.com,
any.example.com, other.com) to point their image URL to a .invalid host (e.g.,
https://allowed.example.invalid/img.png) so fetch() deterministically fails, or
change the assertions to check the validation result from the server (via
runner.fetch response status/body) without depending on downstream network
resolution. Ensure changes reference the VercelEnvRunner instantiation and the
runner.fetch(...) calls in those tests.
package.json (1)

59-81: Document ipx 4.0.0-alpha requirement for image optimization.

ipx@^4.0.0-alpha.1 is a pre-release (stable 3.1.1 available). While the handler has a graceful fallback when ipx is unavailable, there's no documentation explaining why the alpha is required. Consider adding a note to the README clarifying the ipx version requirement and that it's optional—users may be surprised by install-time warnings or unclear why a pre-release is pinned.

Alternatively, pin to an exact version (4.0.0-alpha.1) until a stable 4.x release to prevent accidental prerelease jumps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 59 - 81, The package pins a prerelease ipx range
("ipx": "^4.0.0-alpha.1" in both dependencies and peerDependencies) without
documentation or an exact pin; update either README and package.json: add a
short note in the README explaining that ipx@4.0.0-alpha.1 is required for the
new image optimization handler, that it's optional (referenced by
peerDependenciesMeta "ipx"), and that users may see install-time warnings, or
instead change the package.json entries for "ipx" in dependencies and
peerDependencies to an exact pin ("4.0.0-alpha.1") to avoid accidental
prerelease upgrades until a stable 4.x is available. Ensure the README note
references the handler and the optional peerDependency semantics so users
understand the fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runners/vercel/image.ts`:
- Around line 105-113: The TypeScript error arises because pathname can be
undefined after destructuring sourceUrl.split("?") in validateLocalUrl; ensure
pathname (and search) have explicit string defaults so matchPattern(p.pathname,
pathname) always receives a string—update the destructuring to provide defaults
(e.g., pathname = "" and search = "") and keep the rest of the logic the same so
p.pathname and p.search comparisons remain valid.
- Around line 48-54: resolveWorkerUrl currently returns an invalid URL for unix
socket addresses (when WorkerAddress has socketPath); update resolveWorkerUrl to
not attempt to construct a unix URL: detect "socketPath" in the WorkerAddress
and throw a clear error (or assert) that the image handler only supports TCP
addresses (host/port), and add a short comment documenting that image handling
requires TCP (or alternatively implement an undici fetch path with a custom
Dispatcher using connect:{socketPath} elsewhere if unix sockets must be
supported). Ensure references to WorkerAddress and resolveWorkerUrl remain, and
make the function consistently return only TCP http://host:port... URLs or fail
fast when socketPath is present.

---

Nitpick comments:
In `@package.json`:
- Around line 59-81: The package pins a prerelease ipx range ("ipx":
"^4.0.0-alpha.1" in both dependencies and peerDependencies) without
documentation or an exact pin; update either README and package.json: add a
short note in the README explaining that ipx@4.0.0-alpha.1 is required for the
new image optimization handler, that it's optional (referenced by
peerDependenciesMeta "ipx"), and that users may see install-time warnings, or
instead change the package.json entries for "ipx" in dependencies and
peerDependencies to an exact pin ("4.0.0-alpha.1") to avoid accidental
prerelease upgrades until a stable 4.x is available. Ensure the README note
references the handler and the optional peerDependency semantics so users
understand the fallback.

In `@src/runners/vercel/image.ts`:
- Around line 33-46: The catch branch of loadIPX currently assigns
_ipxLoadResult = false and relies on implicit undefined return; make the
contract explicit by adding an explicit return undefined in the catch so callers
and linters see the intended Promise<IPXModule | undefined> outcome. Update the
catch block inside function loadIPX to set _ipxLoadResult = false, log the
warning as before, then return undefined.
- Around line 321-366: Short-circuit SVG handling by checking the parsed
modifiers.format immediately after parsing the request and before calling
ipx/process(): if modifiers.format === "svg+xml" and config?.dangerouslyAllowSVG
is falsy, return the same 400 Response used for blocked SVGs (matching the
message at the later format check) to avoid running ipx.process(); also change
the catch clause from catch (error: any) to catch (error) and narrow error
(e.g., extract statusCode/message safely) for better type hygiene. Ensure you
reference the existing modifiers, ipx(sourceUrl, modifiers), process(), format
check and the Response construction so the behavior and responses remain
consistent.
- Around line 62-82: The matchPattern function currently treats any pattern
starting with "^" OR ending with "$" as a regex and passes user-supplied input
straight to new RegExp, which mis-classifies globs and can enable ReDoS; update
matchPattern so it only treats a pattern as a regex when it both startsWith("^")
and endsWith("$") (or when an explicit discriminator is present), and avoid
direct unsafe RegExp compilation by validating and constraining the input before
compiling: enforce a sensible max pattern length, sanitize/escape or reject
obviously dangerous constructs, and wrap RegExp construction/test in a try/catch
(or switch to a safe regex library like recheck) so that if compilation fails or
is potentially unsafe you fall back to the glob-to-regex path; reference the
matchPattern function, the pattern variable, and the sites where new
RegExp(pattern) is called and replace those usages accordingly.
- Around line 175-222: getIPX has a race: concurrent callers can each call
loadIPX() and createIPX(), so memoize the pending initialization by introducing
a module-scoped _ipxPromise alongside _ipx; in getIPX, if _ipx exists return it,
else if _ipxPromise exists await it, otherwise set _ipxPromise = (async () => {
const ipxModule = await loadIPX(); if (!ipxModule) return undefined; const
instance = ipxModule.createIPX({...}); _ipx = instance; _ipxPromise = undefined;
return instance; })(); then await and return the resolved instance instead of
calling createIPX multiple times; also update close() to clear both _ipx and
_ipxPromise so subsequent initialization can run cleanly (references: getIPX,
loadIPX, _ipx, _ipxPromise, createIPX, close).

In `@src/runners/vercel/runner.ts`:
- Around line 85-102: The image request branch creates a new Request with only
URL and headers which drops method/body from the original input; update the
logic around requestUrl handling so when constructing the Request passed to
this._imageHandler.handle you propagate the original request semantics (if input
is a Request use input.method, input.body and other relevant properties;
otherwise use init.method and init.body when present) and merge headers,
ensuring this._imageHandler and createVercelImageHandler usage stays the same;
locate symbols requestUrl, input, init, this._imageHandler, and
_imageHandler.handle to apply the change.

In `@test/vercel.test.ts`:
- Around line 267-280: Replace tests that rely on real DNS/remote fetch failures
by using an unroutable reserved TLD (e.g., hostnames under .invalid) or by
asserting validation behavior only; specifically update VercelEnvRunner test
cases (the "allows remote URL when domain matches" test and the other tests
referencing allowed.example.com, cdn.example.com, any.example.com, other.com) to
point their image URL to a .invalid host (e.g.,
https://allowed.example.invalid/img.png) so fetch() deterministically fails, or
change the assertions to check the validation result from the server (via
runner.fetch response status/body) without depending on downstream network
resolution. Ensure changes reference the VercelEnvRunner instantiation and the
runner.fetch(...) calls in those tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4feb6f24-b36e-4e89-8db7-4124470cf9cf

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5d42f and 778aadd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • AGENTS.md
  • package.json
  • pnpm-workspace.yaml
  • src/runners/vercel/image.ts
  • src/runners/vercel/runner.ts
  • test/fixtures/app-image.mjs
  • test/vercel.test.ts

Comment thread src/runners/vercel/image.ts
Comment thread src/runners/vercel/image.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runners/vercel/image.ts`:
- Around line 270-279: The handler currently only validates when
isRemoteUrl(sourceUrl) or sourceUrl.startsWith("/"), allowing unsupported shapes
(e.g., "foo.png", "data:", "file:", uppercase schemes) to fall through; update
the logic in src/runners/vercel/image.ts around the sourceUrl checks so that if
the URL is neither a recognized remote (isRemoteUrl) nor a local absolute path
(startsWith("/")), the function immediately returns a 400 Response('"url"
parameter is not allowed'); ensure this check runs before calling
validateRemoteUrl or validateLocalUrl so unsupported schemes/relative paths are
rejected early.
- Around line 311-320: The accept-header negotiation bypasses the allowed
formats list: when no explicit format param (f) is present the code
unconditionally sets modifiers.format to "avif" or "webp" based only on Accept;
update that branch to consult the configured allowed formats (images.formats /
config.formats) before picking a type. Concretely, inside the block that reads
const accept = request.headers.get("accept") and sets modifiers.format, filter
the candidate formats (["avif","webp",...]) by the configured images.formats
list and only choose the first match present in both the Accept header and the
config; if none match, do not override modifiers.format or fall back to a safe
default already allowed by the config.
- Around line 132-161: The fetchUnoptimized function currently proxies any
upstream response; after performing the fetch (in fetchUnoptimized) inspect the
response Content-Type via res.headers.get('content-type') and reject responses
that are not image/* with an appropriate error status (e.g., 415) instead of
returning the body, and specifically block SVG payloads unless
config?.dangerouslyAllowSVG === true by returning a forbidden error (e.g., 403)
when content-type contains "svg" and dangerouslyAllowSVG is not set; only apply
the vary/cache-control header adjustments and return the Response with res.body
when the content-type checks pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 396dc94f-2d73-4b17-a76d-70c8530c91d7

📥 Commits

Reviewing files that changed from the base of the PR and between 778aadd and b091acb.

📒 Files selected for processing (2)
  • src/runners/vercel/image.ts
  • test/vercel.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/vercel.test.ts

Comment thread src/runners/vercel/image.ts
Comment thread src/runners/vercel/image.ts Outdated
Comment thread src/runners/vercel/image.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runners/vercel/image.ts`:
- Around line 65-83: The matchPattern function currently compiles RegExp on
every request and will throw for malformed patterns; precompile and validate
patterns when loading the runner config and use those cached RegExp objects on
the hot path. Update the config parsing code to convert each pattern string into
a RegExp (or mark it invalid) and store it (e.g., compiledPatterns), log and
skip/disable any patterns that fail compilation, then change matchPattern (or
add a companion like matchCompiledPattern) to accept and test against a
precompiled RegExp rather than calling new RegExp(pattern) per request so the
image route no longer recompiles or crashes on bad patterns.
- Around line 349-353: The parsing of the public ?cache= param
(cacheOverride/cacheTTL) is computed but only used inside the ipx branch, so the
unoptimized fallback path ignores it; update the code so the cache override is
applied in both paths: compute cacheOverride and cacheTTL early (using
cacheOverride = Number.parseInt(...) and cacheTTL as currently defined) and then
use cacheTTL when setting cache-control / TTL in the unoptimized fallback
response as well as in the ipx branch, ensuring the same variable names
(cacheOverride, cacheTTL) are referenced in the fallback code that builds the
response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 463c7629-a5ce-4828-a3ca-0f63cbf8c984

📥 Commits

Reviewing files that changed from the base of the PR and between b091acb and 2c46911.

📒 Files selected for processing (1)
  • src/runners/vercel/image.ts

Comment thread src/runners/vercel/image.ts Outdated
Comment thread src/runners/vercel/image.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runners/vercel/image.ts`:
- Around line 148-166: The fallback path that uses fetchUnoptimized() must apply
the same security and disposition headers as the optimized branch: when building
the Response in the fallback (the block that returns the non-optimized image
response), copy the optimized-branch logic to set content-security-policy from
config.contentSecurityPolicy (if present) and set content-disposition based on
config.contentDispositionType (e.g., "inline" or "attachment" and filename
handling) onto the Headers instance (the same headers variable) before returning
the Response; ensure you also preserve existing vary/cache-control handling
already present so the fallback mirrors the optimized branch.
- Around line 281-291: The code treats protocol-relative URLs like
"//cdn.example.com/a.jpg" as both local and remote; normalize or reject them
before the isLocal/isRemote checks so they are mutually exclusive. Update the
logic in the request handling flow (where isLocal = sourceUrl.startsWith("/")
and isRemote = isRemoteUrl(sourceUrl) are computed) to first detect
protocol-relative URLs and either (a) prepend a default protocol (e.g. "https:")
to produce normalizedSourceUrl, or (b) return a 400 rejecting protocol-relative
URLs; then use normalizedSourceUrl (not the original sourceUrl) for subsequent
branching, validateRemoteUrl(normalizedSourceUrl) /
validateLocalUrl(normalizedSourceUrl), and for the fallback/IPX fetch paths so
remote requests are never treated as local.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b49428d-1f3f-40ac-afd1-cb150ef3b401

📥 Commits

Reviewing files that changed from the base of the PR and between 2c46911 and 2b90dd0.

📒 Files selected for processing (1)
  • src/runners/vercel/image.ts

Comment thread src/runners/vercel/image.ts Outdated
Comment thread src/runners/vercel/image.ts Outdated
@RihanArfan

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runners/vercel/image.ts`:
- Around line 181-183: The fallback response currently sets Vary to "Accept"
only if no Vary exists, which drops Accept when upstream sent a Vary; update the
logic in the fallback response handling (the headers variable in the image
optimization path, e.g., where the code currently checks if
(!headers.has("vary")) headers.set("vary", "Accept")) to always ensure "Accept"
is present by reading existing headers.get("vary"), appending "Accept" if not
already included, and then writing the combined value back to
headers.set("vary", combinedValue) so upstream Vary values are preserved while
guaranteeing Accept is included for correct cache-keying.
- Around line 169-180: The current guard (using headers, contentType and res.ok)
lets through responses with no or non-image Content-Type; change the logic so we
only forward bodies that are positively identified as images: after reading
headers and contentType, if there is no contentType or
/^image\//i.test(contentType) is false, return the 400 Response (rejecting
non-images and missing type); keep the existing SVG check using
config?.dangerouslyAllowSVG and /^image\/svg\+xml\b/i to forbid SVGs when
configured, but ensure that check runs only for responses that passed the
positive image check.
- Around line 159-167: The fetch calls in this file (notably the branches using
getAddress()/resolveWorkerUrl and the external fetches around the code paths
that call ipxHttpStorage) currently follow HTTP redirects and can bypass the
allowlist validation applied only to the original sourceUrl; update those
fetches to either set redirect: 'error' on their RequestInit or implement manual
redirect-following that on each 3xx response reads the Location header and
re-validates the target against the configured allowlist (domains,
remotePatterns, localPatterns) before issuing the next fetch; ensure this change
is applied to the fetch in the sourceUrl local branch (resolveWorkerUrl), the
external fetch branch, and any fetches used by ipxHttpStorage so no redirect hop
can escape the allowlist check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a961a680-cee6-410d-ad3c-d1792a4d0506

📥 Commits

Reviewing files that changed from the base of the PR and between 2b90dd0 and 0f98a8d.

📒 Files selected for processing (1)
  • src/runners/vercel/image.ts

Comment thread src/runners/vercel/image.ts Outdated
Comment thread src/runners/vercel/image.ts Outdated
Comment thread src/runners/vercel/image.ts Outdated
@RihanArfan
RihanArfan requested a review from pi0 May 8, 2026 12:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runners/vercel/image.ts`:
- Around line 294-305: The handler currently defaults quality to 75 and then
always enforces config.qualities, causing requests that omit q to fail when 75
isn't allowed; update the logic around q/quality so that if the incoming q
parameter is absent you either (A) derive the default from config.qualities
(e.g., set quality = config.qualities[0] when config?.qualities?.length) or (B)
skip the config.qualities inclusion check when q was not explicitly provided;
adjust the validation that uses config.qualities.includes(quality) accordingly
so it only runs when q was provided (or after you set the default from
config.qualities). Ensure you reference the existing q, quality and
config.qualities checks in the image handler when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b63161c0-b8c7-4067-8a91-f0903a7665bf

📥 Commits

Reviewing files that changed from the base of the PR and between 2b90dd0 and 8dfcda5.

📒 Files selected for processing (1)
  • src/runners/vercel/image.ts

Comment thread src/runners/vercel/image.ts Outdated
@pi0
pi0 marked this pull request as draft May 22, 2026 00:15
@RihanArfan
RihanArfan marked this pull request as ready for review August 3, 2026 18:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/runners/vercel/image.ts (1)

492-523: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The fallback path ignores the request method.

fetchUnoptimized() always issues a GET and returns res.body, so a HEAD request receives a body from the handler. The ipx path honors HEAD through ipx. Forward request.method to keep both paths equal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runners/vercel/image.ts` around lines 492 - 523, Update the fallback call
to fetchUnoptimized in handle so it receives request.method and preserves HEAD
semantics; ensure fetchUnoptimized uses that method when issuing the upstream
request and does not return a response body for HEAD, matching the ipx path.
.agents/VERCEL.md (1)

71-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix the dangling word in this sentence.

"relied on rather:" reads as an unfinished clause.

✏️ Proposed fix
-Two ipx v4 defaults are relied on rather: `maxOutputDimension` (8192) clamps
+Two ipx v4 defaults are relied on: `maxOutputDimension` (8192) clamps
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/VERCEL.md at line 71, Fix the sentence in the ipx v4 defaults
description by removing or replacing the dangling “rather” before the colon,
while preserving the existing details about maxOutputDimension and SVG
sanitization.
test/vercel-image.test.ts (1)

443-458: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The warn-once assertion depends on test order.

loadIPX() sets _ipxLoaded once per module instance. This test installs the console.warn spy, so it must be the first test in the suite that triggers a load. If a later refactor reorders the suite or adds an earlier test, the first warning happens before the spy exists and this assertion fails silently in intent. Install the spy in beforeAll for this suite, or reset modules per test, to remove the ordering dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/vercel-image.test.ts` around lines 443 - 458, Update the vercel-image
test setup around loadIPX and the “proxies the unoptimized source and warns
once” test so the console.warn spy is installed in a suite-level beforeAll
before any test can trigger loading. Keep the existing single-warning assertion
and restore the spy in suite-level cleanup, eliminating dependence on test
order.
package.json (1)

63-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pin or re-evaluate the ipx prerelease range.

^4.0.0-beta.1 allows later 4.0.0 prereleases and future 4.x stable releases, but lockfile resolution still fixes ipx to 4.0.0-beta.1. If later ipx versions can break the Vercel image API used here (createIPX, ipxHttpStorage, createIPXFetchHandler), narrow the dev dependency and peer dependency to a specific prerelease or stable version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 63, Re-evaluate the ipx version range in package.json
and narrow both the dev dependency and peer dependency to the specific
prerelease or stable version verified for the Vercel image API symbols
createIPX, ipxHttpStorage, and createIPXFetchHandler. Keep the dependency and
peer ranges consistent with the supported version.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/VERCEL.md:
- Line 51: Update the `w` parameter description in `VERCEL.md` to state that it
must be a positive integer, excluding zero, while preserving the existing
bare-integer and parsing restrictions.

In `@src/runners/vercel/image.ts`:
- Around line 85-113: Update patternToRegExp to handle RegExp compilation
failures defensively: catch invalid-pattern errors, avoid caching them as valid
expressions, and return a non-matching result for matchPattern instead of
allowing the exception to escape parseImageRequest or handle. Preserve existing
matching and cache behavior for valid patterns.
- Around line 347-369: Update fetchUnoptimized so both fetch calls use redirect:
"error", preventing an allowlisted source or worker URL from following an
unvalidated Location to another host. Keep the existing URL resolution,
readiness handling, and upstream error response unchanged.

---

Nitpick comments:
In @.agents/VERCEL.md:
- Line 71: Fix the sentence in the ipx v4 defaults description by removing or
replacing the dangling “rather” before the colon, while preserving the existing
details about maxOutputDimension and SVG sanitization.

In `@package.json`:
- Line 63: Re-evaluate the ipx version range in package.json and narrow both the
dev dependency and peer dependency to the specific prerelease or stable version
verified for the Vercel image API symbols createIPX, ipxHttpStorage, and
createIPXFetchHandler. Keep the dependency and peer ranges consistent with the
supported version.

In `@src/runners/vercel/image.ts`:
- Around line 492-523: Update the fallback call to fetchUnoptimized in handle so
it receives request.method and preserves HEAD semantics; ensure fetchUnoptimized
uses that method when issuing the upstream request and does not return a
response body for HEAD, matching the ipx path.

In `@test/vercel-image.test.ts`:
- Around line 443-458: Update the vercel-image test setup around loadIPX and the
“proxies the unoptimized source and warns once” test so the console.warn spy is
installed in a suite-level beforeAll before any test can trigger loading. Keep
the existing single-warning assertion and restore the spy in suite-level
cleanup, eliminating dependence on test order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78e979d7-71bf-41d4-81fa-e33f140d6f43

📥 Commits

Reviewing files that changed from the base of the PR and between 8dfcda5 and 56c0496.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • .agents/VERCEL.md
  • AGENTS.md
  • package.json
  • src/index.ts
  • src/runners/vercel/image.ts
  • src/runners/vercel/runner.ts
  • test/fixtures/app-image.mjs
  • test/vercel-image.test.ts
  • test/vercel.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/fixtures/app-image.mjs
  • AGENTS.md
  • src/runners/vercel/runner.ts

Comment thread .agents/VERCEL.md Outdated
Comment thread src/runners/vercel/image.ts
Comment on lines +347 to +369
async function fetchUnoptimized(
sourceUrl: string,
getAddress: () => WorkerAddress | undefined,
config: VercelImageConfig | undefined,
maxAge: number,
): Promise<Response> {
let res: Response;
try {
if (sourceUrl.startsWith("/")) {
const address = getAddress();
if (!address) {
return new Response("Runner not ready", { status: 503 });
}
res = await fetch(resolveWorkerUrl(address, sourceUrl));
} else {
res = await fetch(sourceUrl);
}
} catch {
// Connection refused, DNS failure, aborted upstream
return new Response('"url" parameter is valid but upstream response is invalid', {
status: 502,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The unoptimized fallback follows redirects, so the allowlist only gates the first hop.

fetch(sourceUrl) and fetch(resolveWorkerUrl(...)) use the default redirect: "follow". An allowlisted remote host can redirect to any host, including an internal address, and the response is then served under the app origin. The ipx path re-validates each hop through ipxHttpStorage({ domains }), so the two paths do not agree. blockPrivateIPs also has no effect here.

Set redirect: "error" on both calls, or follow redirects manually and re-validate each Location with validateRemoteUrl().

🛡️ Proposed fix
-      res = await fetch(resolveWorkerUrl(address, sourceUrl));
+      res = await fetch(resolveWorkerUrl(address, sourceUrl), { redirect: "error" });
     } else {
-      res = await fetch(sourceUrl);
+      res = await fetch(sourceUrl, { redirect: "error" });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runners/vercel/image.ts` around lines 347 - 369, Update fetchUnoptimized
so both fetch calls use redirect: "error", preventing an allowlisted source or
worker URL from following an unvalidated Location to another host. Keep the
existing URL resolution, readiness handling, and upstream error response
unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/runners/vercel/image.ts (2)

364-378: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider an upper bound for w when sizes is not configured.

w is only bounded when config.sizes is set. With the default config, w=100000 reaches ipx and sharp, which allocates a resize buffer proportional to the requested width. A few such requests can exhaust memory in the dev process.

Vercel's endpoint rejects a w outside the configured sizes. Adding a hard ceiling keeps the default path safe without changing the configured behavior.

♻️ Proposed cap
+// Vercel's largest documented device size; also the ceiling `sizes` entries fall under.
+const MAX_WIDTH = 3840;
+
   const width = Number.parseInt(w, 10);
   if (width <= 0) {
     return badRequest('"w" must be a positive integer');
   }
+  if (width > MAX_WIDTH) {
+    return badRequest(`"w" must be less than or equal to ${MAX_WIDTH}`);
+  }
   if (config?.sizes?.length && !config.sizes.includes(width)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runners/vercel/image.ts` around lines 364 - 378, Update the width
validation around the parsed width in the image handler to enforce a hard
maximum when config.sizes is not configured, while preserving the existing
config.sizes membership validation when sizes are provided. Reject values above
the cap with the existing positive-integer validation response or an equivalent
badRequest response.

473-501: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Propagate an abort signal and a timeout to the fallback fetches.

Neither fetch() call receives a signal or a deadline. If the upstream accepts the connection and then stalls, handle() never settles and the request hangs. A client abort also does not cancel the upstream fetch, so the socket stays checked out.

handle() already has request.signal (the runner forwards it in src/runners/vercel/runner.ts). Pass it into fetchUnoptimized() and combine it with a timeout. The same applies to the workerStorage fetches at Lines 564 and 581.

♻️ Proposed signal plumbing
+const UPSTREAM_TIMEOUT = 30_000;
+
 async function fetchUnoptimized(
   sourceUrl: string,
   isLocal: boolean,
   getAddress: () => WorkerAddress | undefined,
   config: VercelImageConfig | undefined,
   maxAge: number,
+  signal?: AbortSignal | null,
 ): Promise<Response> {
+  const timeout = AbortSignal.timeout(UPSTREAM_TIMEOUT);
+  const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
   let res: Response;
   try {
     if (isLocal) {
       const address = getAddress();
       if (!address) {
         return new Response("Runner not ready", { status: 503 });
       }
-      res = await fetch(resolveWorkerUrl(address, sourceUrl));
+      res = await fetch(resolveWorkerUrl(address, sourceUrl), { signal: abort });
     } else {
-      res = await fetch(sourceUrl, { redirect: "error" });
+      res = await fetch(sourceUrl, { redirect: "error", signal: abort });
     }
   } catch {

Then update the call site at Line 651 to pass request.signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runners/vercel/image.ts` around lines 473 - 501, Update handle() and
fetchUnoptimized() to propagate request.signal into both fallback fetches,
combining it with the existing timeout mechanism so stalled upstreams terminate
and client aborts cancel sockets. Apply the same signal-and-timeout handling to
the workerStorage fetches in handle(), and update the fetchUnoptimized call site
to pass request.signal.
.agents/VERCEL.md (1)

92-92: 🚀 Performance & Scalability | 🔵 Trivial

Bound concurrent image encodes before production use.

The documented lack of a sharp concurrency limit allows many requests to allocate image-processing work simultaneously. Add a bounded queue or semaphore, or enforce an upstream concurrency limit before exposing this route to untrusted traffic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/VERCEL.md at line 92, Implement a bounded queue or semaphore to
limit concurrent sharp image encode operations in the image processing route
implementation. Once the concurrency control is in place, update the known gaps
documentation to remove or replace the statement about no cap on concurrent
sharp encodes, as this constraint will then be addressed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/VERCEL.md:
- Line 76: Qualify the statement about reachable 400 responses in the
documentation to refer specifically to “IPX-originated 400s,” preserving the
existing explanation of IPX_INVALID_IMAGE and IPX_INVALID_SVG without implying
that endpoint validation cannot also return 400.
- Line 82: The documentation in the URL validation section lists the
remotePatterns matching fields (protocol, hostname, port, pathname) but omits
explanation of the search parameter that is included in the PR objectives. Add
documentation that clarifies how the search parameter in remotePatterns is
matched against query strings, and explicitly describe the behavior when search
is empty or missing from the configuration. If the search parameter is not
actually implemented, remove it from the PR objectives and align the schema and
tests accordingly instead of documenting it.

---

Nitpick comments:
In @.agents/VERCEL.md:
- Line 92: Implement a bounded queue or semaphore to limit concurrent sharp
image encode operations in the image processing route implementation. Once the
concurrency control is in place, update the known gaps documentation to remove
or replace the statement about no cap on concurrent sharp encodes, as this
constraint will then be addressed.

In `@src/runners/vercel/image.ts`:
- Around line 364-378: Update the width validation around the parsed width in
the image handler to enforce a hard maximum when config.sizes is not configured,
while preserving the existing config.sizes membership validation when sizes are
provided. Reject values above the cap with the existing positive-integer
validation response or an equivalent badRequest response.
- Around line 473-501: Update handle() and fetchUnoptimized() to propagate
request.signal into both fallback fetches, combining it with the existing
timeout mechanism so stalled upstreams terminate and client aborts cancel
sockets. Apply the same signal-and-timeout handling to the workerStorage fetches
in handle(), and update the fetchUnoptimized call site to pass request.signal.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73523f32-893e-4f6b-918f-659da0da9a92

📥 Commits

Reviewing files that changed from the base of the PR and between 56c0496 and e842b15.

📒 Files selected for processing (4)
  • .agents/VERCEL.md
  • src/runners/vercel/image.ts
  • test/vercel-image.test.ts
  • test/vercel.test.ts

Comment thread .agents/VERCEL.md

ipx owns `content-type`, `etag` (weak, from the source `mtime` + modifiers when available, content-hashed otherwise), `if-none-match`/`if-modified-since` → `304`, `last-modified`, `content-security-policy: default-src 'none'` and `x-content-type-options: nosniff`. The handler then overrides `cache-control` (Vercel semantics, `minimumCacheTTL`), ensures `Vary: Accept`, applies the configured CSP/`content-disposition`, and buffers the body to set `content-length` (undici does not derive it from a `Uint8Array` body).

`content-disposition` is built by `contentDisposition()`: quotes and backslashes are stripped from the filename (they would terminate or escape the quoted value) and anything outside printable ASCII is replaced with `_`, with the real name carried in RFC 5987 `filename*` — appended only when it differs, so the common case stays byte-identical to what Vercel sends. The non-ASCII half is not cosmetic: `Headers.set()` throws a `TypeError` on any value above U+00FF, so a source such as `/日本.png` previously took the whole request down with an unhandled rejection rather than returning a `Response`. The filename is percent-decoded first, since a local source is normalized (and therefore encoded) by then. **ipx error bodies are re-stated, statuses are kept.** ipx answers failures with an `HTTPError` JSON body naming its own `IPX_*` code and the resolved source path (`{"status":404,"statusText":"IPX_RESOURCE_NOT_FOUND","message":"Resource not found: /sample.jpg"}`). The statuses are already right — better than a blanket 500 — so `IPX_ERROR_MESSAGES` maps each one onto the plain-text message the rest of the endpoint uses, keeping the status: `404`/`502` → `"url" parameter is valid but upstream response is invalid`, `403` (forbidden host/IP) → `"url" parameter is not allowed`, `400` → `"url" parameter is valid but upstream is not an image`, anything else → `Image optimization failed`. That keeps the ipx and unoptimized-fallback paths answering alike and stops ipx internals reaching the client. The only reachable `400`s are undecodable sources (`IPX_INVALID_IMAGE`, `IPX_INVALID_SVG`) — the modifiers ipx receives are just width/quality/format, all validated by `parseImageRequest()` first. A throw escaping ipx's own error handling is not expected, so it is logged once via `console.warn` before the body is normalized the same way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Qualify the 400 status statement.

“The only reachable 400s” conflicts with the documented 400 responses for missing or disallowed URLs, blocked SVG, and other request validation failures. Change this to “The only IPX-originated 400s” if that is the intended scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/VERCEL.md at line 76, Qualify the statement about reachable 400
responses in the documentation to refer specifically to “IPX-originated 400s,”
preserving the existing explanation of IPX_INVALID_IMAGE and IPX_INVALID_SVG
without implying that endpoint validation cannot also return 400.

Comment thread .agents/VERCEL.md

**Local sources are normalized before they are validated**, and the normalized form is what becomes the fetch id (`new URL(sourceUrl, "http://localhost")` → `pathname + search`). Validating the raw query-param string while fetching it through a URL parser meant the two disagreed, and `localPatterns` could be walked out of: `/assets/../secret.png` lexically satisfies a `/assets/**` rule and then resolves to `/secret.png` on the worker (which the same config denies when asked for directly), and the hand-rolled `split("?")` hid everything past a second `?` from a `search` rule, so `/a.png?v=1?evil=2` passed `search: "?v=1"` and was then fetched with both query strings. The invariant is now that the allowlist sees exactly the path the worker is asked for. Percent-encoding is deliberately left intact rather than decoded, for the same reason (`/assets/..%2Fx` is forwarded as written, so whatever the worker does with it is what was checked) — a `localPatterns` glob therefore matches the encoded pathname, as it does in Next.js.

**URL validation:** Remote sources are **default-deny**, matching Vercel — with neither `domains` nor `remotePatterns` configured, every remote `url` is rejected with 400 `"url" parameter is not allowed`. Allowing them by default would both diverge from production and turn the dev server into an open image proxy. Configured remotes are matched against `domains` (exact hostname) and `remotePatterns` (protocol, hostname glob/regex, port, pathname glob/regex). Local paths are allowed unless narrowed by `localPatterns`. Compiled patterns are cached in a module-level `Map` since they are re-tested on every request. SVG sources are blocked by default (400) unless `dangerouslyAllowSVG` is true.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document remotePatterns.search.

The PR objectives include search in remotePatterns. This section lists protocol, hostname, port, and pathname, but omits query-string matching. Document how search is matched and how empty or missing values behave. If it is unsupported, align the schema and tests instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/VERCEL.md at line 82, The documentation in the URL validation
section lists the remotePatterns matching fields (protocol, hostname, port,
pathname) but omits explanation of the search parameter that is included in the
PR objectives. Add documentation that clarifies how the search parameter in
remotePatterns is matched against query strings, and explicitly describe the
behavior when search is empty or missing from the configuration. If the search
parameter is not actually implemented, remove it from the PR objectives and
align the schema and tests accordingly instead of documenting it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant