Use provider: :aliyun for the native OSS API with V4 authentication. This does not require OSS's optional S3-compatible authentication mode. Existing AWS, Qiniu S3 and custom S3 providers continue to use ExAws.

Configuration

config :my_app, MyApp.Storage,
  provider: :aliyun,
  access_key_id: System.fetch_env!("OSS_ACCESS_KEY_ID"),
  secret_access_key: System.fetch_env!("OSS_ACCESS_KEY_SECRET"),
  region: "cn-hangzhou"

Region is required even with a custom endpoint. Explicit us-east-1 is supported for US (Virginia). Use the region ID without the oss- prefix. The default endpoint is https://{bucket}.oss-{region}.aliyuncs.com; only virtual bucket addressing is supported. Long-term credentials and STS credentials are supported. For STS, set security_token as well as the temporary access key and secret. ExOss does not obtain or refresh STS credentials. Credentials and tokens are redacted from client inspection.

Endpoints

By default, endpoint_mode: :service prefixes the requested bucket to the host:

endpoint: "https://oss-cn-hangzhou-internal.aliyuncs.com"
# bucket.oss-cn-hangzhou-internal.aliyuncs.com

endpoint: "https://oss-accelerate.aliyuncs.com"
# bucket.oss-accelerate.aliyuncs.com (requires acceleration enabled on OSS)

An endpoint already prefixed with the requested bucket is not prefixed twice. For a bucket endpoint or an OSS-bound CNAME, explicitly use:

endpoint: "https://files.example.com",
endpoint_mode: :bucket

A bucket endpoint must refer to the bucket supplied to the operation. Endpoints must be HTTP(S) origins without paths, query strings, fragments or user info. Internal endpoints require network access from the application to OSS.

Upload headers

MyApp.Storage.run(%ExOss.StorageTask{
  source: {:content, png_bytes},
  target: {:cloud, "my-bucket", "images/avatar.png"},
  opts: [
    headers: %{
      "Content-Type" => "image/png",
      "Cache-Control" => "max-age=3600",
      "x-oss-meta-source" => "avatar"
    }
  ]
})

headers accepts a map or list of string name/value pairs. Names are normalized to lowercase. Values are passed through without converting S3 headers to OSS headers. Consult the operation's official API reference for allowed headers and values. ExOss does not maintain a complete list of unsupported provider headers.

opts[:content_type] and its string-key alias opts["content_type"] both populate headers["content-type"]. This same mapping is used by every storage provider. Precedence: explicit header, then legacy content_type, then automatic inference. Local file/content uploads infer from the destination key; remote uploads prefer the source response's Content-Type and otherwise infer from the key for OSS.

Case-insensitive duplicate header names are rejected. ExOss manages Authorization, Host, Date, Content-Length, Transfer-Encoding, Connection, and OSS/AWS signing dates, tokens and payload hashes; callers cannot override these. CR/LF and invalid header names are rejected before signing.

Presigned PUT and GET

credential = MyApp.Storage.upload_credential(
  "my-bucket", "images/avatar.png", 3600,
  headers: %{"content-type" => "image/png", "x-oss-meta-source" => "avatar"}
)

Req.put!(credential.presigned_url,
  body: png_bytes,
  headers: credential.headers
)

The original calls with two or three arguments still work. The optional fourth argument accepts headers and the legacy content_type. No Content-Type is inferred for presigned uploads; only explicitly supplied values are bound to the signature. UploadCredential.headers defaults to %{} and contains the headers the caller must send. OSS V4 signs Content-Type, Content-MD5, all supplied x-oss-* headers and all additional supplied headers. The URL also binds Host. Changing or omitting signed headers invalidates the signature. Setting a Content-Type does not validate the file's bytes.

Downloads accept headers and query_params in the existing options argument:

url = MyApp.Storage.authorize_download_url("my-bucket", "images/avatar.png", 60,
  headers: %{"range" => "bytes=0-1023"},
  query_params: [{"response-content-type", "image/png"}]
)
Req.get!(url, headers: %{"range" => "bytes=0-1023"})

The maximum URL lifetime is 604800 seconds for long-term credentials or 43200 seconds for STS credentials, and at least one second. STS credential expiration may shorten the actual lifetime. Browser direct uploads also require an OSS CORS rule allowing the origin, PUT, and the headers sent by the browser.

Operations and errors

Content and remote uploads use PutObject. Local files use streaming multipart upload with 5 MiB chunks by default (opts[:chunk_size] can increase this). Object headers are sent on multipart initiation; they are not copied blindly to part or completion requests. Parts run sequentially by default; max_concurrency can increase parallelism and timeout controls each part (default 30000 milliseconds). Empty files send one empty part. A failed upload attempts AbortMultipartUpload while preserving the original error; process termination can still leave incomplete uploads, so configure a bucket rule to remove them.

Copy uses OSS CopyObject with x-oss-copy-source and requires source and target buckets in the same region. The API's size and storage-class restrictions apply. Delete and metadata use DeleteObject and HeadObject. Delayed deletion writes delete_after_days; configure the bucket lifecycle rule separately. This is not a deletion scheduler.

Successful run calls retain {:ok, key} / {:ok, nil}. OSS HTTP errors and embedded XML errors return:

{:error, {:oss_error, %{
  status_code: 403,
  code: "AccessDenied",
  message: "...",
  request_id: "..."
}}}

Transport errors return {:error, reason}. Metadata retains the existing {:error, :no_metadata} failure contract. OSS transport options such as receive_timeout or a Req test adapter can be configured under config :ex_oss, :req_opts, .... Signed method, URL, body and headers cannot be overridden through these options. Req auth/query/body transformation options are also ignored after merging global defaults, to protect signed requests. OSS requests do not automatically redirect or retry; callers can decide whether a failed operation is safe to retry.

References

Optional live tests

Default tests use local adapters and deterministic signing vectors. Live tests are excluded by default. To verify PutObject, HEAD, presigned PUT with headers, and presigned GET against a dedicated test bucket:

export OSS_TEST_REGION='cn-hangzhou'
export OSS_TEST_BUCKET='your-test-bucket'
export OSS_TEST_ACCESS_KEY_ID='your-access-key-id'
export OSS_TEST_ACCESS_KEY_SECRET='your-access-key-secret'
# Required with STS temporary credentials:
# export OSS_TEST_SECURITY_TOKEN='your-sts-token'
# Optional service endpoint override:
# export OSS_TEST_ENDPOINT='https://oss-cn-hangzhou.aliyuncs.com'

mix test test/ex_oss/aliyun_integration_test.exs --include aliyun_integration --only oss_live --trace

The CDN test additionally requires OSS_TEST_CDN_ENDPOINT and OSS_TEST_CDN_AUTH_KEY; optional OSS_TEST_CDN_AUTH_TYPE is a, b, c or f, and OSS_TEST_CDN_TTL defaults to 1800. The CDN test uses the default path format for C and default decimal/query parameter names for F. See Aliyun CDN for console setup, environment variables and a command to run only the CDN test. Run both with:

mix test test/ex_oss/aliyun_integration_test.exs --include aliyun_integration

To open a signed download URL manually, set OSS_TEST_KEEP_OBJECTS=1. This prints the URL (valid for 300 seconds from generation) and the object key, and disables automatic cleanup. Delete the printed object from the test bucket after checking it. Signed URLs grant temporary access; avoid sharing the output.

These tests create uniquely named objects below ex-oss-tests/ with a filename containing Chinese characters, a space, + and %. Unless retention is enabled, they attempt to delete them on exit. Use a test bucket with matching CDN origin configuration. The signing key needs object upload, read and delete permissions. Running the live tests performs real cloud requests; unit test success alone does not confirm a particular account's permissions, CORS or CDN configuration. The live tests do not cover multipart uploads, STS expiration or browser CORS.