Skip to content

feat: add Goa.Clients.S3#76

Merged
Im5tu merged 3 commits into
mainfrom
feat/s3-client
Jun 18, 2026
Merged

feat: add Goa.Clients.S3#76
Im5tu merged 3 commits into
mainfrom
feat/s3-client

Conversation

@Im5tu

@Im5tu Im5tu commented Jun 12, 2026

Copy link
Copy Markdown
Owner

What

Fills in the Goa.Clients.S3 stub with a working, AOT-clean S3 client: PutObject / GetObject / HeadObject / DeleteObject. Mirrors the SQS/Lambda client conventions (ErrorOr results, source-gen logging, XmlReader error parsing, request builder, AddS3 DI).

Built to back a document-attachments feature in another service that streams files through its API, but the package is general-purpose.

Notable points

  • Signing: S3 requires the SigV4 canonical URI to be single-encoded, unlike other AWS services. Added an opt-in HttpOptions.UseSingleUriEncoding request option; the default double-encoding path and every existing client are untouched (verified by the existing AWS SDK comparison tests). Also fixed a latent Core bug where the Host header omitted a non-default port while the signed canonical host included it (only bites on non-default ports, e.g. LocalStack).
  • Path-traversal hardening: object keys with empty/./.. segments or control chars, and invalid bucket names, are rejected before URI construction. Without this, new Uri(...) normalizes dot-segments and can reroute a request (e.g. a .. key turning DeleteObject into DeleteBucket).
  • Virtual-host and path-style (ForcePathStyle) addressing; SSE-KMS headers; validated object metadata; Range GET; idempotent delete semantics.

Tests

  • LocalStack integration with signature validation enabled (S3_SKIP_SIGNATURE_VALIDATION=0): round-trips including percent-encoded, unicode, and zero-byte keys, range GET, missing key, path-style. Note: the LocalStack community image doesn't strictly enforce SigV4, so signing correctness is additionally proven by an AWS SDK AWS4Signer comparison test for a percent-encoded path.
  • Key/bucket validation unit tests. Core 22/22, S3 104/104, Release build 0 warnings.

🤖 Generated with Claude Code

…ject)

AOT-clean S3 client mirroring the SQS/Lambda client style: ErrorOr results,
source-gen logging, XmlReader error parsing, builder + DI (AddS3).

- Virtual-host and path-style (ForcePathStyle) addressing; keys encoded
  segment-by-segment preserving '/'
- Reject object keys with empty/./.. segments or control chars, and
  validate bucket names, before URI construction (prevents Uri
  normalization from rerouting requests, e.g. DeleteObject -> DeleteBucket)
- SSE-KMS headers, object metadata (validated HTTP-token names), Range GET
- Error mapping: 404/NoSuchKey/NoSuchBucket -> NotFound, 403 -> Forbidden,
  bodyless HEAD 404, DeleteObject 204-always

core: add opt-in HttpOptions.UseSingleUriEncoding so the signer uses the
once-encoded canonical URI required by S3 (default double-encoding path and
all existing clients unchanged); fix Host header to include non-default
port to match the signed canonical host. Option keys made readonly.

Tests: LocalStack integration (round-trips incl. percent-encoded/unicode/
zero-byte keys, range, missing-key, path-style) with signature validation
enabled; AWS SDK signer comparison for the single-encoding path; key/bucket
validation units. Core 22, S3 104.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Im5tu, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 hour, 12 minutes, and 10 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69c4c2f0-510c-47f0-9cd5-bfae49c09439

📥 Commits

Reviewing files that changed from the base of the PR and between b4b978b and d397b99.

📒 Files selected for processing (8)
  • src/Clients/Goa.Clients.S3/Errors/S3ErrorCodes.cs
  • src/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectBuilder.cs
  • src/Clients/Goa.Clients.S3/README.md
  • src/Clients/Goa.Clients.S3/S3RequestValidation.cs
  • src/Clients/Goa.Clients.S3/S3ServiceClient.cs
  • src/Clients/Goa.Clients.S3/ServiceExtensions.cs
  • tests/Clients/Goa.Clients.S3.Tests/S3KeyValidationTests.cs
  • tests/Clients/Goa.Clients.S3.Tests/S3TestFixture.cs
📝 Walkthrough

Walkthrough

This PR implements the Goa.Clients.S3 client library. The RequestSigner is extended with an UseSingleUriEncoding option and a corrected Host header computation. The new library exposes IS3Client with four operations (PutObjectAsync, GetObjectAsync, HeadObjectAsync, DeleteObjectAsync), S3-specific input validation, a fluent PutObjectBuilder, a S3ServiceClient implementation, and DI registration. Unit tests cover validation and the builder; integration tests use a LocalStack container.

Changes

Goa.Clients.S3 Implementation

Layer / File(s) Summary
Project and solution wiring
Directory.Packages.props, Goa.sln, src/Clients/Goa.Clients.S3/Goa.Clients.S3.csproj, tests/Clients/Goa.Clients.Core.Tests/Goa.Clients.Core.Tests.csproj
Bumps AWSSDK.Core, adds AWSSDK.S3 to central packages, registers Goa.Clients.S3.Tests in the solution, adds ErrorOr and InternalsVisibleTo to the S3 project, and adds AWSSDK.S3 to the Core tests project.
SigV4 single-URI-encoding and Host header fixes
src/Clients/Goa.Clients.Core/Http/HttpOptions.cs, src/Clients/Goa.Clients.Core/Http/RequestSigner.cs
Adds UseSingleUriEncoding option key (makes all option key fields readonly). RequestSigner corrects Host header to use IdnHost with explicit non-default port, and ComputeSignatureCore conditionally bypasses double-encoding when UseSingleUriEncoding is set.
S3 public contracts
src/Clients/Goa.Clients.S3/IS3Client.cs, src/Clients/Goa.Clients.S3/Errors/S3ErrorCodes.cs, src/Clients/Goa.Clients.S3/Operations/*/..., src/Clients/Goa.Clients.S3/S3ServiceClientConfiguration.cs, src/Clients/Goa.Clients.S3/README.md
Defines IS3Client and all request/response sealed classes for Put/Get/Head/Delete operations. Introduces S3ErrorCodes string constants, S3ServiceClientConfiguration with ForcePathStyle, removes placeholder Class1, and expands the README with full usage examples.
S3 input validation and PutObjectBuilder
src/Clients/Goa.Clients.S3/S3RequestValidation.cs, src/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectBuilder.cs
S3RequestValidation implements ValidateKey (rejects dot-traversal, control chars, empty segments) and ValidateBucketName (S3 naming rules), returning ErrorOr. PutObjectBuilder provides a fluent API with HTTP-token validation for metadata names.
S3ServiceClient implementation and DI registration
src/Clients/Goa.Clients.S3/S3ServiceClient.cs, src/Clients/Goa.Clients.S3/ServiceExtensions.cs, src/Clients/Goa.Clients.S3/Log.cs
S3ServiceClient implements all four operations with validation, virtual-host/path-style URI selection, S3-specific key percent-encoding, SSE and x-amz-meta-* header mapping, XML error body parsing, and ErrorOr error mapping. ServiceExtensions registers IS3Client as transient with two AddS3 overloads.
SigV4 single-encoding comparison tests
tests/Clients/Goa.Clients.Core.Tests/Http/RequestSignerComparisonTests.cs
Adds three test cases asserting Goa's single-URI-encoding matches AWS SDK signatures for percent-encoded S3 paths, differs from default double-encoding, and matches default for unreserved paths.
S3 unit tests: validation and key rejection
tests/Clients/Goa.Clients.S3.Tests/S3RequestValidationTests.cs, tests/Clients/Goa.Clients.S3.Tests/PutObjectBuilderTests.cs, tests/Clients/Goa.Clients.S3.Tests/S3KeyValidationTests.cs
Unit tests covering ValidateKey/ValidateBucketName error and success cases, PutObjectBuilder.AddMetadata token validation, and traversal-key rejection for all four operations before transport.
S3 integration test infrastructure and end-to-end tests
tests/Clients/Goa.Clients.S3.Tests/Goa.Clients.S3.Tests.csproj, tests/Clients/Goa.Clients.S3.Tests/LocalStackFixture.cs, tests/Clients/Goa.Clients.S3.Tests/S3TestFixture.cs, tests/Clients/Goa.Clients.S3.Tests/S3ClientIntegrationTests.cs
LocalStackFixture starts a LocalStack S3 container with SigV4 enabled. S3TestFixture builds DI service providers and a test bucket. S3ClientIntegrationTests covers put/get/head/delete round-trips, range reads, metadata, percent-encoded keys, zero-byte bodies, path-style addressing, and dot-segment validation errors.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant S3ServiceClient
    participant S3RequestValidation
    participant RequestSigner
    participant S3

    Caller->>S3ServiceClient: PutObjectAsync(request)
    S3ServiceClient->>S3RequestValidation: ValidateBucketName(bucket)
    S3ServiceClient->>S3RequestValidation: ValidateKey(key)
    S3RequestValidation-->>S3ServiceClient: ErrorOr~Success~
    S3ServiceClient->>S3ServiceClient: BuildObjectUri (virtual-host or path-style + EncodeKey)
    S3ServiceClient->>S3ServiceClient: Set Content-Type, SSE headers, x-amz-meta-* headers
    S3ServiceClient->>RequestSigner: SendAsync (UseSingleUriEncoding=true)
    RequestSigner->>S3: SigV4-signed PUT request
    S3-->>RequestSigner: HTTP 200 + ETag
    RequestSigner-->>S3ServiceClient: HttpResponseMessage
    alt 2xx
        S3ServiceClient-->>Caller: ErrorOr~PutObjectResponse~ (ETag)
    else non-2xx
        S3ServiceClient->>S3ServiceClient: ReadErrorAsync (parse XML body, map 404/403)
        S3ServiceClient-->>Caller: ErrorOr~Error~ (NotFound / Forbidden / Failure)
    end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.54% 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
Title check ✅ Passed The title 'feat: add Goa.Clients.S3' directly and clearly describes the main change—adding a complete S3 client implementation to the Goa library.
Description check ✅ Passed The description is comprehensive and highly relevant, explaining what was implemented, notable technical points (signing, path-traversal hardening, addressing modes), and test coverage.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/s3-client

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

@Im5tu

Im5tu commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/Clients/Goa.Clients.S3.Tests/S3KeyValidationTests.cs (1)

18-31: ⚡ Quick win

Dispose the root ServiceProvider used to create the static client.

CreateClient() builds a container but never disposes it. In test suites this can leave handlers/providers alive and create avoidable resource leaks.

Suggested cleanup
 public class S3KeyValidationTests
 {
     private const string InvalidKeyErrorCode = "S3.InvalidKey";
+    private static ServiceProvider? _serviceProvider;
 
     // Points at an unroutable endpoint - invalid keys must be rejected before any request is sent.
     private static readonly IS3Client Client = CreateClient();
 
     private static IS3Client CreateClient()
     {
         var services = new ServiceCollection();
@@
-        return services.BuildServiceProvider().GetRequiredService<IS3Client>();
+        _serviceProvider = services.BuildServiceProvider();
+        return _serviceProvider.GetRequiredService<IS3Client>();
     }
+
+    [OneTimeTearDown]
+    public void TearDown()
+    {
+        _serviceProvider?.Dispose();
+    }
🤖 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 `@tests/Clients/Goa.Clients.S3.Tests/S3KeyValidationTests.cs` around lines 18 -
31, The CreateClient() method builds a ServiceProvider from the
ServiceCollection but never disposes it, causing resource leaks in tests. Store
the result of BuildServiceProvider() in a variable, wrap it in a using statement
to ensure proper disposal, and retrieve the IS3Client service before the using
block exits so the provider is cleaned up after the client is obtained.
🤖 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/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectBuilder.cs`:
- Around line 136-145: The Build() method in PutObjectBuilder is returning the
internal _metadata dictionary by reference, which allows reusing the builder to
mutate metadata on previously created requests. Change the Metadata assignment
from _metadata to create and return a copy of the dictionary (such as using new
Dictionary<> constructor or .ToDictionary()) to ensure each built
PutObjectRequest has its own independent copy and remains immutable from
subsequent builder calls.

In `@src/Clients/Goa.Clients.S3/README.md`:
- Around line 156-160: The code snippet in the README demonstrating error
handling with GetObjectAsync and ErrorType.NotFound is missing the required
using statement. Add "using ErrorOr;" at the beginning of the code snippet
before the var result = await _s3.GetObjectAsync line to ensure the code can
compile when copied and pasted by users.

In `@src/Clients/Goa.Clients.S3/S3ServiceClient.cs`:
- Around line 57-61: In the PutObjectAsync method, metadata validation is
missing before headers are added to requestMessage. Create a private validation
helper method (similar to the validation logic used in
PutObjectBuilder.AddMetadata) to validate metadata keys and values as valid HTTP
tokens. Before the foreach loop that iterates through request.Metadata, add
validation to check each entry's Key and Value against the same rules enforced
by the builder, throwing an appropriate exception if validation fails. This
ensures that invalid metadata cannot reach the TryAddWithoutValidation call,
regardless of whether PutObjectRequest is constructed through the builder or
used directly.

In `@src/Clients/Goa.Clients.S3/ServiceExtensions.cs`:
- Around line 44-50: The AddS3 method overload that accepts
Action<S3ServiceClientConfiguration> parameter allows configuration with only a
ServiceUrl without applying the region fallback logic that exists in another
overload, creating inconsistent behavior. Before the validation check at line 47
that ensures either region or service URL is provided, apply the same region
fallback logic used in the other AddS3 overload (referenced at Line 28) to
normalize the configuration and prevent request-sign failures when AWS_REGION
environment variable is unset.

In `@tests/Clients/Goa.Clients.S3.Tests/S3TestFixture.cs`:
- Around line 95-101: The bucket cleanup in the S3TestFixture teardown only
processes the first page of objects returned by ListObjectsV2Async, which means
subsequent pages are left behind. When DeleteBucketAsync is called on a
non-empty bucket, it fails and leaves cleanup warnings. Modify the cleanup logic
to handle pagination by checking the IsTruncated property on the
ListObjectsV2Response and continuously fetch subsequent pages using the
ContinuationToken until all objects are retrieved and deleted before calling
DeleteBucketAsync.

---

Nitpick comments:
In `@tests/Clients/Goa.Clients.S3.Tests/S3KeyValidationTests.cs`:
- Around line 18-31: The CreateClient() method builds a ServiceProvider from the
ServiceCollection but never disposes it, causing resource leaks in tests. Store
the result of BuildServiceProvider() in a variable, wrap it in a using statement
to ensure proper disposal, and retrieve the IS3Client service before the using
block exits so the provider is cleaned up after the client is obtained.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a6d68793-9283-410e-ab34-39bcb1f64db0

📥 Commits

Reviewing files that changed from the base of the PR and between 2d712ad and b4b978b.

📒 Files selected for processing (32)
  • Directory.Packages.props
  • Goa.sln
  • src/Clients/Goa.Clients.Core/Http/HttpOptions.cs
  • src/Clients/Goa.Clients.Core/Http/RequestSigner.cs
  • src/Clients/Goa.Clients.S3/Class1.cs
  • src/Clients/Goa.Clients.S3/Errors/S3ErrorCodes.cs
  • src/Clients/Goa.Clients.S3/Goa.Clients.S3.csproj
  • src/Clients/Goa.Clients.S3/IS3Client.cs
  • src/Clients/Goa.Clients.S3/Log.cs
  • src/Clients/Goa.Clients.S3/Operations/DeleteObject/DeleteObjectRequest.cs
  • src/Clients/Goa.Clients.S3/Operations/DeleteObject/DeleteObjectResponse.cs
  • src/Clients/Goa.Clients.S3/Operations/GetObject/GetObjectRequest.cs
  • src/Clients/Goa.Clients.S3/Operations/GetObject/GetObjectResponse.cs
  • src/Clients/Goa.Clients.S3/Operations/HeadObject/HeadObjectRequest.cs
  • src/Clients/Goa.Clients.S3/Operations/HeadObject/HeadObjectResponse.cs
  • src/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectBuilder.cs
  • src/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectRequest.cs
  • src/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectResponse.cs
  • src/Clients/Goa.Clients.S3/README.md
  • src/Clients/Goa.Clients.S3/S3RequestValidation.cs
  • src/Clients/Goa.Clients.S3/S3ServiceClient.cs
  • src/Clients/Goa.Clients.S3/S3ServiceClientConfiguration.cs
  • src/Clients/Goa.Clients.S3/ServiceExtensions.cs
  • tests/Clients/Goa.Clients.Core.Tests/Goa.Clients.Core.Tests.csproj
  • tests/Clients/Goa.Clients.Core.Tests/Http/RequestSignerComparisonTests.cs
  • tests/Clients/Goa.Clients.S3.Tests/Goa.Clients.S3.Tests.csproj
  • tests/Clients/Goa.Clients.S3.Tests/LocalStackFixture.cs
  • tests/Clients/Goa.Clients.S3.Tests/PutObjectBuilderTests.cs
  • tests/Clients/Goa.Clients.S3.Tests/S3ClientIntegrationTests.cs
  • tests/Clients/Goa.Clients.S3.Tests/S3KeyValidationTests.cs
  • tests/Clients/Goa.Clients.S3.Tests/S3RequestValidationTests.cs
  • tests/Clients/Goa.Clients.S3.Tests/S3TestFixture.cs
💤 Files with no reviewable changes (1)
  • src/Clients/Goa.Clients.S3/Class1.cs

Comment thread src/Clients/Goa.Clients.S3/Operations/PutObject/PutObjectBuilder.cs
Comment thread src/Clients/Goa.Clients.S3/README.md
Comment thread src/Clients/Goa.Clients.S3/S3ServiceClient.cs
Comment thread src/Clients/Goa.Clients.S3/ServiceExtensions.cs
Comment thread tests/Clients/Goa.Clients.S3.Tests/S3TestFixture.cs Outdated
- PutObjectBuilder.Build() now copies metadata so built requests are
  independent of later builder mutations
- Validate metadata HTTP-token names for directly-constructed PutObject
  requests via shared S3RequestValidation.ValidateMetadata (reused by builder)
- AddS3(Action<>) overload applies AWS_REGION/us-east-1 fallback so SigV4
  signing works when only a ServiceUrl is configured
- README error-handling snippet includes `using ErrorOr;`
- S3TestFixture cleanup paginates ListObjectsV2 before deleting the bucket
- S3KeyValidationTests disposes its ServiceProvider via [After(Class)]
@Im5tu Im5tu merged commit c3f223c into main Jun 18, 2026
2 of 3 checks passed
@Im5tu Im5tu deleted the feat/s3-client branch June 18, 2026 11:31
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