net.s3, net.http: add an S3-compatible client and a scheme-handler dispatch in http.fetch#27008
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 509dabe79b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Returns an error if neither bucket nor key is provided. | ||
| pub fn build_object_path(creds Credentials, bucket_override string, key string) !string { | ||
| bucket := if bucket_override != '' { bucket_override } else { creds.bucket } | ||
| clean_key := strip_slashes(key) |
There was a problem hiding this comment.
Preserve leading/trailing slashes in object keys
build_object_path currently normalizes keys with strip_slashes(key), which changes valid S3 object names (for example, folder/ becomes folder, and /x becomes x). S3 keys are byte-exact identifiers, so this causes put/get/stat/delete to address a different object than requested whenever callers intentionally use leading or trailing / characters.
Useful? React with 👍 / 👎.
| if amz_end := endpoint.index('.amazonaws.com') { | ||
| if s3_pos := endpoint.index('s3.') { | ||
| start := s3_pos + 3 | ||
| if start < amz_end { | ||
| return endpoint[start..amz_end] | ||
| } |
There was a problem hiding this comment.
Default AWS global endpoint to us-east-1 region
When endpoint is s3.amazonaws.com and no explicit region is provided, this parser does not extract a region and falls back to 'auto', which is then used in the SigV4 credential scope. AWS S3 expects us-east-1 for the global endpoint, so requests signed with region auto will fail authentication unless users manually set region.
Useful? React with 👍 / 👎.
| if part_number > max_parts { | ||
| first_err = new_error('TooManyParts', | ||
| 'Exceeded ${max_parts} parts — increase part_size') | ||
| break |
There was a problem hiding this comment.
Allow multipart uploads with exactly 10,000 parts
The TooManyParts guard runs after incrementing part_number, so an upload that legitimately uses exactly max_parts parts is still marked as an error right after enqueueing the final allowed part. This rejects protocol-valid uploads at the S3 limit (10,000 parts), e.g. very large uploads with small configured part sizes.
Useful? React with 👍 / 👎.
| .put, .post { | ||
| c.put(key, opts.body, | ||
| bucket: bucket |
There was a problem hiding this comment.
Honor POST method in s3.fetch uploads
The .post branch is grouped with .put and calls c.put(...), which always signs and sends a PUT request. As a result, callers requesting method: .post silently send the wrong method on the wire, which breaks endpoints/workflows that require POST semantics.
Useful? React with 👍 / 👎.
… off-by-one, reject fetch .post
|
Pushed P1 — P1 — P2 — In P2 —
|
|
Thanks!
I agree. |
S3 is one of the most widely deployed cloud APIs across the major providers and self-hosted object stores. Having a first-class client in V, with no third-party dependencies and no need to shell out to aws-cli, makes the language a credible choice for cloud workloads.
I am proposing this module for native integration under
vlib/net/s3because of how common the stack is, but please tell me if you would rather see it land undervlib/x/first. The module is self-contained either way.vlib/net/s3is an S3-compatible client with no third-party dependencies. Signing is built oncrypto.hmacandcrypto.sha256only, and is verified against the AWS-published Signature V4 reference vectors.It exposes single-shot ops (
put,get,get_string,stat,exists,size,delete,presign,list), bucket ops (create_bucket,delete_bucket,bucket_existswith client-side validation of S3 bucket-naming rules), and multipart upload. The multipart layer coversupload_file(auto single/multipart),upload_file_multipart,upload_bytes_multipart, plus a streamingMultipartUploaderfor chunks generated on the fly, with up toClient.queue_sizeparallel part uploads with per-part retry and exponential back-off.A small File handle (
Client.file(key)returning a reference type that forwards to the client viaf.read(),f.write(),f.presign(), and friends) is provided for ergonomic call sites.s3.fetch('s3://bucket/key', ...)acts as a direct entry point, plus an init() registers thes3://scheme with net.http sohttp.fetch(url: 's3://...')works once the module is imported.Credentials.from_env()resolves credentials with first-non-empty resolution across S3_, AWS_, CELLAR_ADDON_, SCW_, B2_, R2_, SPACES_* so the same client targets the major hosted and self-hosted S3 providers with no reconfiguration.A short usage example:
vlib/net/http/http.vadds a tiny scheme-handler registry to let modules likenet.s3plug intohttp.fetchfor non-HTTP schemes.register_scheme(scheme, handler)andunregister_scheme(scheme)let modules claim a scheme from their owninit().fetch()looks up a handler for any non-empty, non-HTTP/HTTPS scheme and delegates if one is registered. Otherwise the call path is byte-for-byte identical to master, with no behaviour change for plain HTTP calls. The new__global scheme_handlersmap is opt-in via@[has_globals]on the module, per V's standard convention.On the architecture side,
client.vholds the Client struct, single-shot CRUD and thedo_http core.multipart.vholds the multipart upload, the parallel worker pool and the MultipartUploader.bucket.vhandles bucket lifecycle and bucket-name validation.signer.vis pure SigV4 (no HTTP coupling) and exposessign_requestandpresign_url.credentials.vholds the Credentials struct, env merging and region resolution.encoding.vhas URI percent-encoding, hex helpers and CRLF guards.errors.vdefines S3Error and parses the XML envelope.fetch.vimplements thes3.fetch('s3://...')URL helper.http_bridge.vregisters thes3://scheme withnet.http.file.vexposes the File handle bound to a Client and a key.list.vparses ListObjectsV2 XML.types.vgathers the shared option and response structs (Acl, StorageClass and friends).The signer is intentionally decoupled from
net.http: it is a pure function that takes credentials and a SignRequest and returns the headers. This keeps it testable against the published SigV4 vectors and reusable outside the HTTP client.On the security side, every user-supplied value going into a signed header or signed query param is screened for CR/LF before signing (InvalidHeader, InvalidQueryParam, InvalidCredentials). The signer rejects anything outside
GET,PUT,POST,DELETEandHEAD.s3.fetchonly acceptss3://URLs,https://,http://,file://and friends are rejected up-front to prevent accidental cross-protocol calls with S3 credentials. The default scheme is HTTPS whenever endpoint does not carry an explicithttp://prefix andinsecure_httpis not set. When compiled with-d s3_debug, request headers are dumped but Authorization andX-Amz-Security-Tokenvalues are masked.redact_urlstrips query strings before logging so presigned URLs never leak signature material into error text.The unit suite covers 8 files and runs offline by default.
v test vlib/net/s3/reports 8 passed, 8 total. The tests cover signer vectors (AWS reference, canonical request, HMAC chain cross-checked with openssl), encoding, error parsing, credentials env merging, bucket-name validation, list-response parsing, fetch URL parsing, redaction and CRLF rejection.Integration tests live in
integration_test.v, gated onS3_INTEGRATION=1. They exercise put/get round-trip, range read, presign GET, list with prefix, exists/delete,s3.fetchand thehttp.fetch('s3://...')bridge, multipart (parallel, with out-of-order completion), bucket lifecycle and an invalid-credentials cleanup-error path. Example invocation:S3_INTEGRATION=1 \ S3_HOST=https://s3.example.com \ S3_KEY_ID=... S3_KEY_SECRET=... \ S3_BUCKET=v-s3-tests \ v test vlib/net/s3/integration_test.vThe module has been verified against AWS S3 SigV4 reference vectors (byte-exact Authorization header matches in signer_test.v), against a local self-hosted S3-compatible server in debug and -prod builds (full integration suite green), and against a hosted S3-compatible production endpoint (full integration suite green).
This work builds on top of #27000 (HEAD, 1xx, 204 and 304 responses on Content-Length-bearing connections), which had to land first for stat, exists and range responses to behave correctly under V's HTTP client.