Skip to content

feat: Add an endpoint override to the r2 adapter - #133

Merged
haydenbleasel merged 2 commits into
haydenbleasel:mainfrom
zachdunn:r2-endpoint-override
Aug 12, 2026
Merged

feat: Add an endpoint override to the r2 adapter#133
haydenbleasel merged 2 commits into
haydenbleasel:mainfrom
zachdunn:r2-endpoint-override

Conversation

@zachdunn

@zachdunn zachdunn commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What

Adds an optional endpoint to the r2 adapter — on R2HttpOptions and on R2BindingOptions (hybrid signing mode) — falling back to the current default https://<accountId>.r2.cloudflarestorage.com at all three construction sites (aws-sdk client, fetch client, hybrid signer).

Why

The r2 wrapper delegates to the s3 machinery, which already accepts endpoint publicly — the wrapper just seals that knob shut by hardcoding the hostname. That makes two real cases unreachable:

  • Jurisdiction buckets. R2 buckets created with a jurisdiction (eu, fedramp) live on their own hostnames (https://<accountId>.eu.r2.cloudflarestorage.com); the r2 adapter can't talk to them at all today, which locks out GDPR/FedRAMP-constrained users.
  • S3-compatible stand-ins. Pointing the r2 code path at MinIO/LocalStack in CI, or at an S3-speaking proxy/gateway.

Notes

  • Fully backwards compatible — omitted endpoint behaves exactly as before.
  • We (uploads.sh) have been running this as a pnpm patch in production to support jurisdiction buckets as BYO storage; upstreaming so we can drop the patch.
  • Tests cover all three sites: aws-sdk client config, fetch-client request/presign hosts, and the hybrid binding signing fallback.

Summary by CodeRabbit

  • New Features

    • Added optional custom endpoint configuration for R2 connections.
    • Supports jurisdiction-specific R2 hostnames and S3-compatible services.
    • accountId is optional when a custom endpoint is provided.
    • Custom endpoints work consistently across HTTP, hybrid binding, signing, and generated URLs.
    • Existing configurations continue using the standard R2 endpoint by default.
  • Documentation

    • Added release documentation for the endpoint override option.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
files-sdk Ready Ready Preview Aug 12, 2026 1:53am

Request Review

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 784604a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
files-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The R2 SDK adds optional endpoint overrides to HTTP and hybrid binding options. The override applies to signing, fetch requests, and the lazy AWS SDK adapter. Tests cover custom hosts and generated URLs.

Changes

R2 endpoint override

Layer / File(s) Summary
Endpoint option contracts
.changeset/r2-endpoint-override.md, packages/files-sdk/src/r2/index.ts
R2HttpOptions and R2BindingOptions now accept an optional endpoint. The changeset documents default-host fallback and supported custom hosts.
Endpoint propagation
packages/files-sdk/src/r2/index.ts
Hybrid signing, fetch-based HTTP requests, and the lazy AWS SDK adapter use the configured endpoint. Each path retains the account-based endpoint when no override is provided.
Custom endpoint validation
packages/files-sdk/test/r2.test.ts
Tests verify custom hosts for HTTP requests, hybrid presigned URLs, fetch uploads, and generated URLs.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: haydenbleasel

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding an endpoint override to the R2 adapter.
Description check ✅ Passed The description clearly explains the changes, motivation, compatibility, supported use cases, and test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@zachdunn zachdunn changed the title Add an endpoint override to the r2 adapter feat: Add an endpoint override to the r2 adapter Aug 11, 2026
accountId only ever feeds the default endpoint hostname, so an explicit
endpoint stands in for it — both in the HTTP path (MinIO/LocalStack
stand-ins no longer need a dummy accountId) and in hybrid binding mode
(the signing fallback now activates on endpoint + credentials + bucket).
@haydenbleasel

Copy link
Copy Markdown
Owner

pushed 784604a on top: accountId is now optional when you pass an explicit endpoint — it only ever fed the default hostname, so the minio/localstack case doesn't need a dummy account id anymore, and hybrid signing now activates on endpoint + creds + bucket without one. also updated the tsdoc/error messages and added tests for both paths. thanks for upstreaming this!

@haydenbleasel
haydenbleasel merged commit e6e6444 into haydenbleasel:main Aug 12, 2026
15 of 16 checks passed

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/files-sdk/src/r2/index.ts (1)

71-79: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Define the accepted endpoint URL shape.

The fetch consumer builds path-style URLs from endpointUrl.origin. An endpoint such as https://proxy.example/s3 therefore loses the /s3 prefix, so fetch and hybrid requests target the wrong path.

Either preserve the endpoint pathname in packages/files-sdk/src/internal/s3-fetch.ts, or reject non-root paths consistently for both clients. Add regression tests for R2HttpOptions.endpoint and R2BindingOptions.endpoint.

Also applies to: 140-147

🤖 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 `@packages/files-sdk/src/r2/index.ts` around lines 71 - 79, Define and enforce
the endpoint URL shape consistently for R2HttpOptions and R2BindingOptions.
Preserve endpointUrl.pathname when constructing path-style requests in the fetch
client, or reject non-root paths in both clients; ensure the selected behavior
is shared consistently. Add regression tests covering endpoint values for both
option types, including a non-root pathname.
🤖 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 `@packages/files-sdk/src/r2/index.ts`:
- Line 360: Update the responseContentDisposition validation error near its
guard to accept either accountId or endpoint, matching the hybrid-signing
guidance in the updated R2 binding error while preserving the existing
credential requirements.
- Around line 335-348: The R2 configuration currently hardcodes the SigV4
region; add an optional region property to both R2 option types, defaulting to
"auto", and propagate that resolved value through the hybrid s3FetchAdapter path
and the fetch and AWS SDK configuration paths near the referenced option
construction points. Ensure custom endpoints can override the default while
existing callers retain "auto".

---

Outside diff comments:
In `@packages/files-sdk/src/r2/index.ts`:
- Around line 71-79: Define and enforce the endpoint URL shape consistently for
R2HttpOptions and R2BindingOptions. Preserve endpointUrl.pathname when
constructing path-style requests in the fetch client, or reject non-root paths
in both clients; ensure the selected behavior is shared consistently. Add
regression tests covering endpoint values for both option types, including a
non-root pathname.
🪄 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: 7e18babf-04dc-440a-bb8f-2cdb263c218d

📥 Commits

Reviewing files that changed from the base of the PR and between 7802d9c and 784604a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .changeset/r2-endpoint-override.md
  • packages/files-sdk/src/r2/index.ts
  • packages/files-sdk/test/r2.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/r2-endpoint-override.md
  • packages/files-sdk/test/r2.test.ts

Comment on lines +335 to +348
// An explicit `endpoint` stands in for `accountId`, which only ever feeds
// the default signing hostname.
const signerEndpoint =
opts.endpoint ??
(opts.accountId
? `https://${opts.accountId}.r2.cloudflarestorage.com`
: undefined);
const hybrid =
// oxlint-disable-next-line sonarjs/expression-complexity -- the inline && chain is what narrows each opt to a non-undefined string inside the branch; extracting the guard loses that narrowing
opts.accountId && opts.accessKeyId && opts.secretAccessKey && httpBucket
signerEndpoint && opts.accessKeyId && opts.secretAccessKey && httpBucket
? s3FetchAdapter({
accessKeyId: opts.accessKeyId,
bucket: httpBucket,
endpoint: `https://${opts.accountId}.r2.cloudflarestorage.com`,
endpoint: signerEndpoint,

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'region:\s*"auto"|endpoint|R2HttpOptions|R2BindingOptions' \
  packages/files-sdk/src/r2/index.ts \
  packages/files-sdk/src/internal/s3-fetch.ts \
  packages/files-sdk/test/r2.test.ts

Repository: haydenbleasel/files-sdk

Length of output: 24270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- R2 option definitions and construction sites ---'
sed -n '35,155p;585,660p' packages/files-sdk/src/r2/index.ts

printf '%s\n' '--- Shared S3 option definitions and adapters ---'
rg -n -C 5 'interface S3AdapterOptions|region\??:|s3FetchAdapter|new S3Client|region' packages/files-sdk/src packages/files-sdk/test/r2.test.ts

printf '%s\n' '--- R2 package documentation and configuration examples ---'
rg -n -i -C 4 'endpoint|region|LocalStack|MinIO|compatible' packages/files-sdk README.md packages/files-sdk/test 2>/dev/null | head -n 300

Repository: haydenbleasel/files-sdk

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Shared adapter region contract ---'
rg -n -C 8 'export interface S3AdapterOptions|interface S3AdapterOptions|region\??:' packages/files-sdk/src/s3 packages/files-sdk/src/internal

printf '%s\n' '--- All R2 region references and tests ---'
rg -n -C 3 'region|endpoint|client: "fetch"|client: "aws-sdk"' packages/files-sdk/src/r2/index.ts packages/files-sdk/test/r2.test.ts

printf '%s\n' '--- Package metadata and dependency versions ---'
rg -n -C 3 '"aws4fetch"|"`@aws-sdk/client-s3`"|localstack|minio' package.json packages/files-sdk/package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | head -n 160

Repository: haydenbleasel/files-sdk

Length of output: 25179


🌐 Web query:

LocalStack S3 SigV4 signing region us-east-1 configured region documentation

💡 Result:

When using LocalStack for S3, the region is typically set to us-east-1, which is the default for AWS SDKs and tools when connecting to LocalStack [1][2][3]. LocalStack supports SigV4 signing, which is standard for S3 requests [4]. Key configuration details for S3 and region handling in LocalStack: 1. Region Configuration: LocalStack supports multi-region emulation [5][6]. While us-east-1 is the commonly used default [6][7], you must ensure your client is explicitly configured to use this region, especially if you have enabled multi-region features [5][8]. 2. AWS SDK Configuration: When using SDKs (such as the AWS SDK for Java), you should set the region to Regions.US_EAST_1.getName and point the endpoint to the LocalStack S3 service (typically s3.localhost.localstack.cloud:4566) [1][2]. 3. AWS CLI Configuration: You can set the region using environment variables or profile configurations [3]: - Environment Variable: export AWS_DEFAULT_REGION=us-east-1 - AWS Config File: Set region=us-east-1 in your [profile localstack] configuration [3]. 4. SigV4 Signing: LocalStack uses SigV4 for request validation, including for pre-signed URLs [9][10][4]. If you encounter signature validation issues, you can use the S3_SKIP_SIGNATURE_VALIDATION flag (set to 1 to skip, 0 to validate) [5][8]. Note that for validation to pass, your AWS_SECRET_ACCESS_KEY must be set to test, or you must be using credentials returned from STS [5][8]. For most development scenarios, configuring your client with the standard us-east-1 region and pointing the endpoint to LocalStack is sufficient for proper SigV4 interaction [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from hashlib import sha256
from hmac import new as hmac_new

def signing_key(secret: bytes, date: bytes, region: bytes) -> bytes:
    k_date = hmac_new(b"AWS4" + secret, date, sha256).digest()
    k_region = hmac_new(k_date, region, sha256).digest()
    k_service = hmac_new(k_region, b"s3", sha256).digest()
    return hmac_new(k_service, b"aws4_request", sha256).digest()

date = b"20260801"
secret = b"SECRET"
for region in (b"auto", b"us-east-1"):
    scope = date + b"/" + region + b"/s3/aws4_request"
    key = signing_key(secret, date, region)
    print(region.decode(), "scope:", scope.decode(), "key:", key.hex())

assert signing_key(secret, date, b"auto") != signing_key(
    secret, date, b"us-east-1"
)
assert b"/auto/s3/aws4_request" != b"/us-east-1/s3/aws4_request"
print("Different regions produce different SigV4 scopes and signing keys.")
PY

Repository: haydenbleasel/files-sdk

Length of output: 462


Make the SigV4 region configurable for custom endpoints.

region: "auto" produces a different SigV4 scope from us-east-1, which can reject requests to LocalStack or another region-validating S3-compatible service. Add an optional region to both R2 option types, default it to "auto", and pass it through the hybrid, fetch, and AWS SDK paths at Lines 352, 625, and 654.

🤖 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 `@packages/files-sdk/src/r2/index.ts` around lines 335 - 348, The R2
configuration currently hardcodes the SigV4 region; add an optional region
property to both R2 option types, defaulting to "auto", and propagate that
resolved value through the hybrid s3FetchAdapter path and the fetch and AWS SDK
configuration paths near the referenced option construction points. Ensure
custom endpoints can override the default while existing callers retain "auto".

throw new FilesError(
"Provider",
"r2 binding: signing requires either `publicBaseUrl` (for url()) or HTTP credentials (`accountId`, `accessKeyId`, `secretAccessKey`, `bucket`) for presigned URLs. See https://developers.cloudflare.com/r2/api/s3/tokens/."
"r2 binding: signing requires either `publicBaseUrl` (for url()) or HTTP credentials (`accountId` or `endpoint`, plus `accessKeyId`, `secretAccessKey`, `bucket`) for presigned URLs. See https://developers.cloudflare.com/r2/api/s3/tokens/."

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

Keep hybrid-signing error guidance consistent.

The updated error accepts accountId or endpoint, but the responseContentDisposition guard at Line 559 still tells callers to pass accountId only. Endpoint-only configuration is valid, so that branch gives incorrect remediation. Use the same accountId-or-endpoint wording in both messages.

🤖 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 `@packages/files-sdk/src/r2/index.ts` at line 360, Update the
responseContentDisposition validation error near its guard to accept either
accountId or endpoint, matching the hybrid-signing guidance in the updated R2
binding error while preserving the existing credential requirements.

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.

2 participants