Skip to content

Release v4.1.0

Choose a tag to compare

@gmoon gmoon released this 13 Jul 00:55
· 26 commits to master since this release
44ea628

v4.1.0

This is the first published 4.x release. 4.0.0 was never shipped to npm, so if
you are upgrading from 3.x this is a major, breaking release — read the
"Breaking changes" section below. Full details and code examples are in
MIGRATION.md.

s3proxy still does the same thing: stream S3 objects to HTTP responses without
buffering them on your server. v4 fixes v3's error contract and splits the
288-line class into a parser, a gateway, and an orchestrator.

Breaking changes (3.x -> 4.x)

  • Typed errors instead of empty 200 streams. In v3 a missing key returned a
    fully-formed empty stream with status 200; AccessDenied, InvalidRange, and
    NoSuchBucket were absorbed the same way. v4 throws typed errors —
    S3NotFound (404), S3Forbidden (403), S3InvalidRange (416),
    InvalidRequest (400) — all extending S3ProxyError (with statusCode and
    the underlying SDK error as cause). These throw from fetch() before
    res.writeHead, so a v3 stream.on('error') handler will no longer see
    them; wrap the await in try/catch instead.
  • proxy.get(req, res) / proxy.head(req, res) / proxy.healthCheckStream(res)
    removed.
    These mutated the response as a side effect. v4 ships one pure
    entry point, proxy.fetch(req), returning { stream, status, headers }.
    Use req.method = 'HEAD' for HEAD, and proxy.healthCheck() (throws on
    failure) for health endpoints.
  • Response shape renamed: v3's s3stream / statusCode are now stream /
    status (headers unchanged).
  • Parser helpers are free functions. S3Proxy.parseRequest /
    mapHeaderToParam / stripLeadingSlash moved to package-root exports:
    import { parseRequest } from 's3proxy'. parseRequest now throws
    InvalidRequest for malformed percent-encoding and null-byte keys.
  • S3Proxy.isNonFatalError removed — use instanceof S3ProxyError.
  • HttpRequest is now a structural type ({ url, method?, headers, path?, query? }), no longer extends IncomingMessage.

Added

  • verifyOnInit: false — skip the health check on init() so a transient
    S3 hiccup can't crashloop a pod before readiness probes take over.
  • proxy.pipe(req, res) — fetch an object and stream it straight to an HTTP
    response. Recovers v3's one-call proxy.get(req, res) ergonomics without
    the empty-200 bug: a missing or forbidden object renders an honest
    404/403/416.
  • proxy.middleware() — an Express/Connect-style request handler built on
    pipe(): app.get('/*splat', proxy.middleware()). Forwards unexpected
    errors to next when present.
  • proxy.staticSite(options) — replicates S3 static website hosting:
    index-document resolution (/ and /dir/ -> index.html) and, when
    errorDocument is set, serving that key under the original 4xx status for
    missing/forbidden objects. An opt-in layer over fetch() — it can't mask a
    real failure.

Fixed

  • Header passthrough. v4.0 rebuilt response headers from the typed SDK
    output and silently dropped x-amz-meta-* custom metadata plus several
    S3-specific headers (server-side encryption, KMS key id, version id, storage
    class, website redirect, expiration, restore). All are forwarded again, so
    callers that round-trip custom metadata work as they did in v3.

Health checks

The v3 healthCheckStream(res) wrapper is gone; v4 exposes the primitive
proxy.healthCheck() (pings the bucket, throws S3ProxyError on failure) and
you render the response. For a load-balancer /health endpoint, try/catch and
return 200 or the error's status. For orchestrators (ECS/Kubernetes), construct
with verifyOnInit: false so init() does not ping S3 at boot — a transient
hiccup then can't crashloop the pod before dashboards surface why — and let a
/ready handler call healthCheck() to drive readiness. See the README
"Health Checks" and "verifyOnInit for orchestrators" sections for full
examples.

Requirements

  • ESM-only, Node.js 22.13+, AWS SDK v3.

Staying on v3

npm install s3proxy@^3.0.0 keeps the old behavior; it just doesn't get the
typed-error contract, the parser/gateway split, or verifyOnInit.