Skip to content

Conversation

@michaelshimeles
Copy link
Owner

@michaelshimeles michaelshimeles commented Dec 10, 2025

User description

This PR was created by a Graphite background agent: https://app.graphite.com/background-agents/michaelshimeles/nextjs-starter-kit/task/bgt_01kc4x8pcbeg6spy6zpfarthnb

Fix this pr

#58


PR Type

Enhancement


Description

  • Replace hardcoded R2 URL with configurable environment variable

  • Add URL validation and normalization in upload function

  • Extract hostname from R2_PUBLIC_URL for Next.js image config

  • Make remote image patterns conditional based on configuration


Diagram Walkthrough

flowchart LR
  env["R2_PUBLIC_URL env var"]
  upload["uploadImageAssets function"]
  config["next.config.ts"]
  images["images.remotePatterns"]
  
  env -- "normalize & validate" --> upload
  env -- "extract hostname" --> config
  config -- "conditional pattern" --> images
Loading

File Walkthrough

Relevant files
Enhancement
upload-image.ts
Make R2 URL configurable with validation                                 

lib/upload-image.ts

  • Replace hardcoded R2 URL with configurable R2_PUBLIC_URL environment
    variable
  • Add validation to throw error if environment variable is missing
  • Normalize base URL by removing trailing slash before constructing
    public URL
+7/-1     
next.config.ts
Extract and conditionally configure R2 hostname                   

next.config.ts

  • Add getR2Hostname() function to extract hostname from R2_PUBLIC_URL
  • Handle missing protocol by prepending https:// if needed
  • Add error handling with console warning for invalid URLs
  • Make remote image patterns conditional using spread operator based on
    hostname availability
+28/-4   
Documentation
.env.example
Document R2_PUBLIC_URL configuration variable                       

.env.example

  • Add R2_PUBLIC_URL environment variable with documentation comment
  • Specify expected format including protocol (e.g.,
    https://your-bucket.r2.dev)
+2/-0     

@vercel
Copy link
Contributor

vercel bot commented Dec 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
nextstarter Error Error Dec 10, 2025 7:58pm

...(r2Hostname
? [
{
protocol: "https" as const,
Copy link

Choose a reason for hiding this comment

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

Protocol is hardcoded to https but R2_PUBLIC_URL may specify http://. This creates a mismatch where upload-image.ts generates URLs with the actual protocol from R2_PUBLIC_URL, but Next.js Image will only allow HTTPS.

For example, if R2_PUBLIC_URL=http://localhost:9000, uploads will generate http://localhost:9000/image.jpg but Next.js will reject it because the remote pattern only allows https://localhost:9000.

Fix by extracting the protocol:

const getR2Config = (): { protocol: 'http' | 'https'; hostname: string } | null => {
  const r2PublicUrl = process.env.R2_PUBLIC_URL;
  if (!r2PublicUrl) return null;
  try {
    const urlWithProtocol = r2PublicUrl.startsWith("http")
      ? r2PublicUrl
      : `https://${r2PublicUrl}`;
    const url = new URL(urlWithProtocol);
    return {
      protocol: url.protocol.replace(':', '') as 'http' | 'https',
      hostname: url.hostname
    };
  } catch {
    console.warn(`Invalid R2_PUBLIC_URL: ${r2PublicUrl}`);
    return null;
  }
};

const r2Config = getR2Config();

// Then use:
protocol: r2Config.protocol,
hostname: r2Config.hostname,
Suggested change
protocol: "https" as const,
protocol: r2Config?.protocol || "https" as const,

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@michaelshimeles michaelshimeles marked this pull request as ready for review December 10, 2025 20:04
@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Insecure object ACL

Description: The use of S3-style ACLs with ACL: "public-read" can unintentionally expose uploaded
objects if the bucket is not intended to be public; prefer bucket policies or omit ACLs to
avoid accidental public access.
upload-image.ts [18-23]

Referred Code
  Bucket: process.env.R2_UPLOAD_IMAGE_BUCKET_NAME!,
  Key: key,
  Body: buffer,
  ContentType: "image/*",
  ACL: "public-read", // optional if bucket is public
})
Unvalidated URL usage

Description: Constructing the public URL directly from R2_PUBLIC_URL without validating scheme or
hostname allows misconfiguration or malicious values (e.g., javascript:, internal
hostnames) to be propagated to clients; restrict to https and validate with URL parsing.
upload-image.ts [26-33]

Referred Code
const baseUrl = process.env.R2_PUBLIC_URL;
if (!baseUrl) {
  throw new Error("R2_PUBLIC_URL environment variable is not configured");
}
// Normalize: remove trailing slash if present
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
const publicUrl = `${normalizedBase}/${key}`;
return publicUrl;
Untrusted domain whitelisting

Description: The hostname is derived from R2_PUBLIC_URL by prepending https if missing, but the value
is not restricted to expected domains; a compromised env var could whitelist arbitrary
external hosts for Next.js image optimization, enabling SSRF-like fetches in image loader.

next.config.ts [3-18]

Referred Code
const getR2Hostname = (): string | null => {
  const r2PublicUrl = process.env.R2_PUBLIC_URL;
  if (!r2PublicUrl) {
    return null;
  }
  try {
    // Add protocol if missing to ensure URL constructor works
    const urlWithProtocol = r2PublicUrl.startsWith("http")
      ? r2PublicUrl
      : `https://${r2PublicUrl}`;
    return new URL(urlWithProtocol).hostname;
  } catch {
    console.warn(`Invalid R2_PUBLIC_URL: ${r2PublicUrl}`);
    return null;
  }
};
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled error: Throwing a raw Error when R2_PUBLIC_URL is missing lacks contextual handling and does not
provide a safe fallback or structured logging for production debugging.

Referred Code
if (!baseUrl) {
  throw new Error("R2_PUBLIC_URL environment variable is not configured");
}
// Normalize: remove trailing slash if present
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
const publicUrl = `${normalizedBase}/${key}`;
return publicUrl;

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logs: The new image upload action and configuration validation add no audit logging (user,
action, outcome), which may be acceptable if auditing is handled elsewhere.

Referred Code
export const uploadImageAssets = async (buffer: Buffer, key: string) => {
  await r2.send(
    new PutObjectCommand({
      Bucket: process.env.R2_UPLOAD_IMAGE_BUCKET_NAME!,
      Key: key,
      Body: buffer,
      ContentType: "image/*",
      ACL: "public-read", // optional if bucket is public
    })
  );

  const baseUrl = process.env.R2_PUBLIC_URL;
  if (!baseUrl) {
    throw new Error("R2_PUBLIC_URL environment variable is not configured");
  }
  // Normalize: remove trailing slash if present
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
  const publicUrl = `${normalizedBase}/${key}`;
  return publicUrl;

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Console warning: Logging the invalid R2_PUBLIC_URL value to console in production builds may expose
configuration details; unclear if this is gated by environment.

Referred Code
try {
  // Add protocol if missing to ensure URL constructor works
  const urlWithProtocol = r2PublicUrl.startsWith("http")
    ? r2PublicUrl
    : `https://${r2PublicUrl}`;
  return new URL(urlWithProtocol).hostname;
} catch {
  console.warn(`Invalid R2_PUBLIC_URL: ${r2PublicUrl}`);
  return null;
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Logs config value: The console.warn includes the raw R2_PUBLIC_URL value, which could be sensitive in some
environments; confirm logging policies for config values.

Referred Code
try {
  // Add protocol if missing to ensure URL constructor works
  const urlWithProtocol = r2PublicUrl.startsWith("http")
    ? r2PublicUrl
    : `https://${r2PublicUrl}`;
  return new URL(urlWithProtocol).hostname;
} catch {
  console.warn(`Invalid R2_PUBLIC_URL: ${r2PublicUrl}`);
  return null;
}

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Centralize R2 URL validation and normalization

The parsing logic for R2_PUBLIC_URL is inconsistent between next.config.ts and
lib/upload-image.ts. This should be centralized into a shared utility to ensure
consistent URL validation and normalization.

Examples:

lib/upload-image.ts [26-32]
  const baseUrl = process.env.R2_PUBLIC_URL;
  if (!baseUrl) {
    throw new Error("R2_PUBLIC_URL environment variable is not configured");
  }
  // Normalize: remove trailing slash if present
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
  const publicUrl = `${normalizedBase}/${key}`;
next.config.ts [3-18]
const getR2Hostname = (): string | null => {
  const r2PublicUrl = process.env.R2_PUBLIC_URL;
  if (!r2PublicUrl) {
    return null;
  }
  try {
    // Add protocol if missing to ensure URL constructor works
    const urlWithProtocol = r2PublicUrl.startsWith("http")
      ? r2PublicUrl
      : `https://${r2PublicUrl}`;

 ... (clipped 6 lines)

Solution Walkthrough:

Before:

// In lib/upload-image.ts
export const uploadImageAssets = async (buffer, key) => {
  const baseUrl = process.env.R2_PUBLIC_URL;
  if (!baseUrl) {
    throw new Error("R2_PUBLIC_URL is not configured");
  }
  // Simple normalization, but no protocol validation
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
  return `${normalizedBase}/${key}`;
};

// In next.config.ts
const getR2Hostname = () => {
  const r2PublicUrl = process.env.R2_PUBLIC_URL;
  // More robust parsing, adds protocol if missing
  const urlWithProtocol = r2PublicUrl.startsWith("http")
    ? r2PublicUrl
    : `https://${r2PublicUrl}`;
  return new URL(urlWithProtocol).hostname;
};

After:

// In a new shared file, e.g., lib/r2-config.ts
export function getR2Config() {
  const r2PublicUrl = process.env.R2_PUBLIC_URL;
  if (!r2PublicUrl) return null;
  try {
    const urlWithProtocol = r2PublicUrl.startsWith("http") ? r2PublicUrl : `https://${r2PublicUrl}`;
    const url = new URL(urlWithProtocol);
    return {
      hostname: url.hostname,
      baseUrl: url.origin, // e.g., https://your-bucket.r2.dev
    };
  } catch {
    return null;
  }
}

// In lib/upload-image.ts (updated)
import { getR2Config } from './r2-config';
export const uploadImageAssets = async (buffer, key) => {
  const r2Config = getR2Config();
  if (!r2Config) throw new Error("R2_PUBLIC_URL is not configured or invalid");
  return `${r2Config.baseUrl}/${key}`;
};
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a significant design flaw where inconsistent handling of R2_PUBLIC_URL across two files can lead to invalid URLs, and proposes a robust solution by centralizing the logic.

Medium
Possible issue
Enforce HTTPS protocol for R2 URL

In getR2Hostname, validate that the protocol for R2_PUBLIC_URL is https. If it
is not, log a warning and return null to prevent runtime errors with Next.js
image optimization, which expects https.

next.config.ts [3-18]

 const getR2Hostname = (): string | null => {
   const r2PublicUrl = process.env.R2_PUBLIC_URL;
   if (!r2PublicUrl) {
     return null;
   }
   try {
     // Add protocol if missing to ensure URL constructor works
     const urlWithProtocol = r2PublicUrl.startsWith("http")
       ? r2PublicUrl
       : `https://${r2PublicUrl}`;
-    return new URL(urlWithProtocol).hostname;
+    const url = new URL(urlWithProtocol);
+    if (url.protocol !== "https:") {
+      console.warn(`R2_PUBLIC_URL must use https protocol. Found: ${url.protocol}`);
+      return null;
+    }
+    return url.hostname;
   } catch {
     console.warn(`Invalid R2_PUBLIC_URL: ${r2PublicUrl}`);
     return null;
   }
 };
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential misconfiguration where an http URL would be accepted but fail at runtime due to the hardcoded https protocol in remotePatterns, improving the code's robustness.

Medium
  • More

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant