All Products
Search
Document Center

Object Storage Service:Upload files using signed URLs (Go SDK V2)

Last Updated:Mar 20, 2026

OSS bucket objects are private by default — only the object owner can access them. Presigned URLs let you delegate temporary upload access to third parties without sharing your credentials. Generate a presigned PUT URL with the Go SDK V2, share it, and anyone with the URL can upload an object until it expires. The URL can be reused multiple times before expiry.

Prerequisites

Before you begin, ensure that you have:

  • An OSS bucket

  • Access credentials with the oss:PutObject permission on the target bucket (required for the user who generates the presigned URL; generating the presigned URL itself requires no special permissions for the uploader)

  • The OSS Go SDK V2 installed: github.com/aliyun/alibabacloud-oss-go-sdk-v2

How it works

Presigning separates the party that authorizes uploads from the party that performs them:

  1. The object owner (your server) calls client.Presign() with a PutObjectRequest and an expiry duration. The SDK signs the request using Signature V4 and embeds the signature in the URL's query parameters.

  2. The owner shares the presigned URL with a third party.

  3. The third party sends an HTTP PUT request directly to OSS using the URL — no OSS credentials required.

  4. OSS verifies the embedded signature. If valid and the URL has not expired, OSS accepts the upload.

Important

Treat presigned URLs as bearer tokens. Anyone who has the URL can upload an object until it expires. Share URLs only with intended recipients and use short expiry durations for sensitive operations.

The diagram below shows the full flow:

image

The Presign method

func (c *Client) Presign(ctx context.Context, request any, optFns ...func(*PresignOptions)) (result *PresignResult, err error)

Parameters

ParameterTypeDescription
ctxcontext.ContextRequest context
request*PutObjectRequestThe operation to presign (use PutObjectRequest for uploads)
optFns...func(*PresignOptions)Optional. Time-to-live (TTL) options. Defaults to 15 minutes if omitted

`PresignOptions`

OptionTypeDescription
Expirestime.DurationDuration from now until the URL expires. For example, 30 * time.Minute.
Expirationtime.TimeAbsolute expiration time. Takes precedence over Expires if both are set.
Important

For Signature V4, the maximum TTL is 7 days.

Return values

Presign returns a *PresignResult:

FieldTypeDescription
MethodstringHTTP method for the operation (PUT for PutObjectRequest)
URLstringThe presigned URL
Expirationtime.TimeWhen the URL expires
SignedHeadersmap[string]stringRequest headers that are part of the signature. The uploader must send these exact headers when using the URL.

Generate a presigned URL

The code sample below uses the cn-hangzhou region and a public endpoint. For access from other Alibaba Cloud services in the same region, use an internal endpoint instead. See Regions and endpoints.

Access credentials are read from environment variables. See Configure access credentials.

package main

import (
	"context"
	"flag"
	"log"
	"time"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)

var (
	region     string
	bucketName string
	objectName string
)

func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	flag.Parse()

	if len(bucketName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name required")
	}
	if len(region) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, region required")
	}
	if len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, object name required")
	}

	// Load credentials from environment variables and configure the region.
	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	client := oss.NewClient(cfg)

	// Generate a presigned PUT URL that expires in 10 minutes.
	result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
		Bucket: oss.Ptr(bucketName),
		Key:    oss.Ptr(objectName),
	},
		oss.PresignExpires(10*time.Minute),
	)
	if err != nil {
		log.Fatalf("failed to presign PutObject request: %v", err)
	}

	log.Printf("Method: %v\n", result.Method)
	log.Printf("Expiration: %v\n", result.Expiration)
	log.Printf("URL: %v\n", result.URL)

	// If SignedHeaders is non-empty, the uploader must include these headers
	// in the PUT request. Omitting them causes a signature error.
	if len(result.SignedHeaders) > 0 {
		log.Printf("Signed headers (must be sent by the uploader):\n")
		for k, v := range result.SignedHeaders {
			log.Printf("  %v: %v\n", k, v)
		}
	}
}

Presigned URL structure

The generated URL embeds the signature and expiry in its query parameters:

https://exampleobject.oss-cn-hangzhou.aliyuncs.com/exampleobject.txt
  ?x-oss-date=20241112T083238Z
  &x-oss-expires=3599
  &x-oss-signature-version=OSS4-HMAC-SHA256
  &x-oss-credential=LTAI***%2F20241112%2Fcn-hangzhou%2Foss%2Faliyun_v4_request
  &x-oss-signature=ed5a******
Query parameterDescription
x-oss-dateSigning timestamp in ISO 8601 format
x-oss-expiresTTL in seconds, measured from x-oss-date
x-oss-signature-versionSignature algorithm (OSS4-HMAC-SHA256 for Signature V4)
x-oss-credentialCredential scope used to compute the signature
x-oss-signatureThe computed signature

Share this URL with the uploader. The URL is valid for the duration you specified.

Upload using the presigned URL

The uploader sends an HTTP PUT request with the file as the request body. All examples use <signedUrl> as a placeholder — replace it with the actual URL from the previous step.

Important

If SignedHeaders was non-empty when you generated the URL, the uploader must include those exact headers. Sending different headers causes OSS to return a SignatureDoesNotMatch error.

curl

curl -X PUT -T /path/to/local/file "<signedUrl>"

Go

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func uploadFile(signedUrl, filePath string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("unable to open file: %w", err)
	}
	defer file.Close()

	client := &http.Client{}

	req, err := http.NewRequest("PUT", signedUrl, file)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response: %w", err)
	}

	fmt.Printf("Status: %d\n", resp.StatusCode)
	fmt.Println(string(body))
	return nil
}

func main() {
	signedUrl := "<signedUrl>"
	filePath := "/path/to/local/file"

	if err := uploadFile(signedUrl, filePath); err != nil {
		fmt.Println("Upload error:", err)
	}
}

Java

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;

public class SignUrlUpload {
    public static void main(String[] args) throws Throwable {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;

        URL signedUrl = new URL("<signedUrl>");
        String pathName = "/path/to/local/file";

        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);
            httpClient = HttpClients.createDefault();
            response = httpClient.execute(put);

            System.out.println("Status: " + response.getStatusLine().getStatusCode());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
            httpClient.close();
        }
    }
}

Python

import requests

def upload_file(signed_url, file_path):
    try:
        with open(file_path, 'rb') as file:
            response = requests.put(signed_url, data=file)
        print(f"Status: {response.status_code}")
        print(response.text)
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    signed_url = "<signedUrl>"
    file_path = "/path/to/local/file"
    upload_file(signed_url, file_path)

Node.js

const fs = require('fs');
const axios = require('axios');

async function uploadFile(signedUrl, filePath) {
    try {
        const fileStream = fs.createReadStream(filePath);
        const response = await axios.put(signedUrl, fileStream, {
            headers: { 'Content-Type': 'application/octet-stream' }
        });
        console.log(`Status: ${response.status}`);
    } catch (error) {
        console.error(`Error: ${error.message}`);
    }
}

(async () => {
    const signedUrl = '<signedUrl>';
    const filePath = '/path/to/local/file';
    await uploadFile(signedUrl, filePath);
})();

Browser.js

Important

Browsers automatically add a Content-Type header to PUT requests. If Content-Type was not included when you generated the presigned URL, the signature will not match and OSS returns a 403 SignatureDoesNotMatch error. To avoid this, always specify ContentType in PutObjectRequest when generating URLs for browser-based uploads.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>File Upload</title>
</head>
<body>
    <input type="file" id="fileInput" />
    <button id="uploadButton">Upload</button>

    <script>
        const signedUrl = "<signedUrl>";

        document.getElementById('uploadButton').addEventListener('click', async () => {
            const file = document.getElementById('fileInput').files[0];
            if (!file) {
                alert('Select a file to upload.');
                return;
            }
            try {
                const response = await fetch(signedUrl, {
                    method: 'PUT',
                    body: file,
                });
                if (!response.ok) {
                    throw new Error(`Upload failed: ${response.status}`);
                }
                alert('Uploaded successfully.');
            } catch (error) {
                console.error('Error:', error);
                alert('Upload failed: ' + error.message);
            }
        });
    </script>
</body>
</html>

C#

using System.Net.Http.Headers;

var filePath = "/path/to/local/file";
var presignedUrl = "<signedUrl>";

using var httpClient = new HttpClient();
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var content = new StreamContent(fileStream);

var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl);
request.Content = content;

var response = await httpClient.SendAsync(request);

if (response.IsSuccessStatusCode)
{
    Console.WriteLine($"Upload successful. Status: {response.StatusCode}");
}
else
{
    string responseContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"Upload failed. Status: {response.StatusCode}");
    Console.WriteLine("Response: " + responseContent);
}

C++

#include <iostream>
#include <fstream>
#include <curl/curl.h>

void uploadFile(const std::string& signedUrl, const std::string& filePath) {
    CURL* curl = curl_easy_init();
    if (!curl) return;

    curl_global_init(CURL_GLOBAL_DEFAULT);

    FILE* file = fopen(filePath.c_str(), "rb");
    if (!file) {
        std::cerr << "Unable to open file: " << filePath << std::endl;
        return;
    }

    fseek(file, 0, SEEK_END);
    long fileSize = ftell(file);
    fseek(file, 0, SEEK_SET);

    curl_easy_setopt(curl, CURLOPT_URL, signedUrl.c_str());
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize);
    curl_easy_setopt(curl, CURLOPT_READDATA, file);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        std::cerr << "curl failed: " << curl_easy_strerror(res) << std::endl;
    } else {
        long httpCode = 0;
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
        std::cout << "Status: " << httpCode << std::endl;
    }

    fclose(file);
    curl_easy_cleanup(curl);
    curl_global_cleanup();
}

int main() {
    std::string signedUrl = "<signedUrl>";
    std::string filePath = "/path/to/local/file";
    uploadFile(signedUrl, filePath);
    return 0;
}

Android

package com.example.signurlupload;

import android.os.AsyncTask;
import android.util.Log;

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class SignUrlUploadActivity {
    private static final String TAG = "SignUrlUploadActivity";

    public void uploadFile(String signedUrl, String filePath) {
        new UploadTask().execute(signedUrl, filePath);
    }

    private class UploadTask extends AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            String signedUrl = params[0];
            String filePath = params[1];

            HttpURLConnection connection = null;
            DataOutputStream dos = null;
            FileInputStream fis = null;

            try {
                URL url = new URL(signedUrl);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("PUT");
                connection.setDoOutput(true);
                connection.setRequestProperty("Content-Type", "application/octet-stream");

                fis = new FileInputStream(filePath);
                dos = new DataOutputStream(connection.getOutputStream());

                byte[] buffer = new byte[1024];
                int length;
                while ((length = fis.read(buffer)) != -1) {
                    dos.write(buffer, 0, length);
                }
                dos.flush();

                int responseCode = connection.getResponseCode();
                Log.d(TAG, "Status: " + responseCode);
                return "Upload complete. Status: " + responseCode;
            } catch (IOException e) {
                e.printStackTrace();
                return "Upload failed: " + e.getMessage();
            } finally {
                if (connection != null) connection.disconnect();
            }
        }

        @Override
        protected void onPostExecute(String result) {
            Log.d(TAG, result);
        }
    }

    public static void main(String[] args) {
        SignUrlUploadActivity activity = new SignUrlUploadActivity();
        String signedUrl = "<signedUrl>";
        String filePath = "/path/to/local/file";
        activity.uploadFile(signedUrl, filePath);
    }
}

Best practices

Set short expiry durations. The maximum TTL for Signature V4 is 7 days, but use the shortest duration that works for your use case. This limits the exposure window if a URL is intercepted.

Always specify `Content-Type` for browser uploads. Browsers automatically add a Content-Type header to PUT requests. Include ContentType in PutObjectRequest when generating the presigned URL so the signatures match.

Match signed headers exactly. Any header included in PutObjectRequest at presigning time — such as Content-Type, x-oss-storage-class, or user metadata — becomes part of the signature. The uploader must send those exact headers with the same values. Check result.SignedHeaders to see which headers to pass.

Generate one URL per upload session. Do not cache presigned URLs across user sessions or requests with different parameters. Generate a fresh URL each time to avoid reuse and signature mismatches.

Common scenarios

Upload with request headers and user metadata

When you need to control the storage class or attach metadata, include those fields in PutObjectRequest. The uploader must then send the same headers.

Step 1: Generate the presigned URL (object owner)

result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
	Bucket:       oss.Ptr(bucketName),
	Key:          oss.Ptr(objectName),
	ContentType:  oss.Ptr("text/plain;charset=utf8"),
	StorageClass: oss.StorageClassStandard,
	Metadata:     map[string]string{"key1": "value1", "key2": "value2"},
},
	oss.PresignExpires(10*time.Minute),
)

Step 2: Upload using the presigned URL (third party)

All request headers must match the values used when generating the URL.

curl

curl -X PUT \
     -H "Content-Type: text/plain;charset=utf8" \
     -H "x-oss-storage-class: Standard" \
     -H "x-oss-meta-key1: value1" \
     -H "x-oss-meta-key2: value2" \
     -T "/path/to/local/file" \
     "<signedUrl>"

Go

package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func uploadFile(signedUrl, filePath string, headers, metadata map[string]string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	fileBytes, err := io.ReadAll(file)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("PUT", signedUrl, bytes.NewBuffer(fileBytes))
	if err != nil {
		return err
	}

	for key, value := range headers {
		req.Header.Set(key, value)
	}
	// User metadata keys must be prefixed with x-oss-meta-.
	for key, value := range metadata {
		req.Header.Set(fmt.Sprintf("x-oss-meta-%s", key), value)
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	fmt.Printf("Status: %d\n", resp.StatusCode)
	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
	return nil
}

func main() {
	signedUrl := "<signedUrl>"
	filePath := "/path/to/local/file"

	headers := map[string]string{
		"Content-Type":        "text/plain;charset=utf8",
		"x-oss-storage-class": "Standard",
	}
	metadata := map[string]string{
		"key1": "value1",
		"key2": "value2",
	}

	if err := uploadFile(signedUrl, filePath, headers, metadata); err != nil {
		fmt.Printf("Error: %v\n", err)
	}
}

Java

import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.model.StorageClass;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;
import java.util.*;

public class SignUrlUpload {
    public static void main(String[] args) throws Throwable {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;

        URL signedUrl = new URL("<signedUrl>");
        String pathName = "/path/to/local/file";

        Map<String, String> headers = new HashMap<>();
        headers.put(OSSHeaders.STORAGE_CLASS, StorageClass.Standard.toString());
        headers.put(OSSHeaders.CONTENT_TYPE, "text/plain;charset=utf8");

        // User metadata keys must be prefixed with x-oss-meta-.
        Map<String, String> userMetadata = new HashMap<>();
        userMetadata.put("key1", "value1");
        userMetadata.put("key2", "value2");

        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);

            for (Map.Entry<String, String> header : headers.entrySet()) {
                put.addHeader(header.getKey(), header.getValue());
            }
            for (Map.Entry<String, String> meta : userMetadata.entrySet()) {
                put.addHeader("x-oss-meta-" + meta.getKey(), meta.getValue());
            }

            httpClient = HttpClients.createDefault();
            response = httpClient.execute(put);
            System.out.println("Status: " + response.getStatusLine().getStatusCode());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
            httpClient.close();
        }
    }
}

Python

import requests

def upload_file(signed_url, file_path, headers=None, metadata=None):
    if not headers:
        headers = {}
    if not metadata:
        metadata = {}

    # User metadata keys must be prefixed with x-oss-meta-.
    for key, value in metadata.items():
        headers[f'x-oss-meta-{key}'] = value

    try:
        with open(file_path, 'rb') as file:
            response = requests.put(signed_url, data=file, headers=headers)
        print(f"Status: {response.status_code}")
        print(response.text)
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    signed_url = "<signedUrl>"
    file_path = "/path/to/local/file"
    headers = {
        "Content-Type": "text/plain;charset=utf8",
        "x-oss-storage-class": "Standard",
    }
    metadata = {
        "key1": "value1",
        "key2": "value2",
    }
    upload_file(signed_url, file_path, headers, metadata)

Node.js

const fs = require('fs');
const axios = require('axios');

async function uploadFile(signedUrl, filePath, headers = {}, metadata = {}) {
    try {
        // User metadata keys must be prefixed with x-oss-meta-.
        for (const [key, value] of Object.entries(metadata)) {
            headers[`x-oss-meta-${key}`] = value;
        }

        const fileStream = fs.createReadStream(filePath);
        const response = await axios.put(signedUrl, fileStream, { headers });
        console.log(`Status: ${response.status}`);
    } catch (error) {
        console.error(`Error: ${error.message}`);
    }
}

(async () => {
    const signedUrl = '<signedUrl>';
    const filePath = '/path/to/local/file';
    const headers = {
        'Content-Type': 'text/plain;charset=utf8',
        'x-oss-storage-class': 'Standard',
    };
    const metadata = {
        'key1': 'value1',
        'key2': 'value2',
    };
    await uploadFile(signedUrl, filePath, headers, metadata);
})();

Browser.js

Important

Browsers automatically add a Content-Type header to PUT requests. If Content-Type was not included when you generated the presigned URL, the signature will not match and OSS returns a 403 SignatureDoesNotMatch error. Always specify ContentType in PutObjectRequest when generating URLs for browser-based uploads.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>File Upload</title>
</head>
<body>
    <input type="file" id="fileInput" />
    <button id="uploadButton">Upload</button>

    <script>
        const signedUrl = "<signedUrl>";

        document.getElementById('uploadButton').addEventListener('click', async () => {
            const file = document.getElementById('fileInput').files[0];
            if (!file) {
                alert('Select a file to upload.');
                return;
            }
            try {
                const headers = {
                    'Content-Type': 'text/plain;charset=utf8',
                    'x-oss-storage-class': 'Standard',
                    'x-oss-meta-key1': 'value1',
                    'x-oss-meta-key2': 'value2',
                };
                const response = await fetch(signedUrl, {
                    method: 'PUT',
                    headers,
                    body: file,
                });
                if (!response.ok) {
                    throw new Error(`Upload failed: ${response.status}`);
                }
                alert('Uploaded successfully.');
            } catch (error) {
                console.error('Error:', error);
                alert('Upload failed: ' + error.message);
            }
        });
    </script>
</body>
</html>

C#

using System.Net.Http.Headers;

var filePath = "/path/to/local/file";
var presignedUrl = "<signedUrl>";

using var httpClient = new HttpClient();
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var content = new StreamContent(fileStream);

var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl);
request.Content = content;

// Headers must match those specified when generating the presigned URL.
request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain") { CharSet = "utf8" };
request.Content.Headers.Add("x-oss-meta-key1", "value1");
request.Content.Headers.Add("x-oss-meta-key2", "value2");
request.Content.Headers.Add("x-oss-storage-class", "Standard");

var response = await httpClient.SendAsync(request);

if (response.IsSuccessStatusCode)
{
    Console.WriteLine($"Upload successful. Status: {response.StatusCode}");
}
else
{
    string responseContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"Upload failed. Status: {response.StatusCode}");
    Console.WriteLine("Response: " + responseContent);
}

C++

#include <iostream>
#include <fstream>
#include <curl/curl.h>
#include <map>
#include <string>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) {
    size_t totalSize = size * nmemb;
    output->append((char*)contents, totalSize);
    return totalSize;
}

void uploadFile(const std::string& signedUrl, const std::string& filePath,
                const std::map<std::string, std::string>& headers,
                const std::map<std::string, std::string>& metadata) {
    CURL* curl = curl_easy_init();
    std::string readBuffer;

    curl_global_init(CURL_GLOBAL_DEFAULT);

    if (curl) {
        FILE* file = fopen(filePath.c_str(), "rb");
        if (!file) {
            std::cerr << "Unable to open file: " << filePath << std::endl;
            return;
        }

        fseek(file, 0, SEEK_END);
        long fileSize = ftell(file);
        rewind(file);

        curl_easy_setopt(curl, CURLOPT_URL, signedUrl.c_str());
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_READDATA, file);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize);

        struct curl_slist* chunk = nullptr;
        for (const auto& header : headers) {
            std::string headerStr = header.first + ": " + header.second;
            chunk = curl_slist_append(chunk, headerStr.c_str());
        }
        // User metadata keys must be prefixed with x-oss-meta-.
        for (const auto& meta : metadata) {
            std::string metaStr = "x-oss-meta-" + meta.first + ": " + meta.second;
            chunk = curl_slist_append(chunk, metaStr.c_str());
        }
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);

        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "curl failed: " << curl_easy_strerror(res) << std::endl;
        } else {
            long responseCode;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
            std::cout << "Status: " << responseCode << std::endl;
        }

        fclose(file);
        curl_slist_free_all(chunk);
        curl_easy_cleanup(curl);
    }

    curl_global_cleanup();
}

int main() {
    std::string signedUrl = "<signedUrl>";
    std::string filePath = "/path/to/local/file";

    std::map<std::string, std::string> headers = {
        {"Content-Type", "text/plain;charset=utf8"},
        {"x-oss-storage-class", "Standard"},
    };
    std::map<std::string, std::string> metadata = {
        {"key1", "value1"},
        {"key2", "value2"},
    };

    uploadFile(signedUrl, filePath, headers, metadata);
    return 0;
}

Android

import android.os.AsyncTask;
import android.util.Log;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class SignUrlUploadActivity extends AppCompatActivity {
    private static final String TAG = "SignUrlUploadActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String signedUrl = "<signedUrl>";
        String pathName = "/storage/emulated/0/demo.txt";

        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "text/plain;charset=utf8");
        headers.put("x-oss-storage-class", "Standard");

        // User metadata keys must be prefixed with x-oss-meta-.
        Map<String, String> userMetadata = new HashMap<>();
        userMetadata.put("key1", "value1");
        userMetadata.put("key2", "value2");

        new UploadTask().execute(signedUrl, pathName, headers, userMetadata);
    }

    private class UploadTask extends AsyncTask<Object, Void, Integer> {
        @Override
        protected Integer doInBackground(Object... params) {
            String signedUrl = (String) params[0];
            String pathName = (String) params[1];
            Map<String, String> headers = (Map<String, String>) params[2];
            Map<String, String> userMetadata = (Map<String, String>) params[3];

            try {
                URL url = new URL(signedUrl);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("PUT");
                connection.setDoOutput(true);
                connection.setUseCaches(false);

                for (Map.Entry<String, String> header : headers.entrySet()) {
                    connection.setRequestProperty(header.getKey(), header.getValue());
                }
                for (Map.Entry<String, String> meta : userMetadata.entrySet()) {
                    connection.setRequestProperty("x-oss-meta-" + meta.getKey(), meta.getValue());
                }

                File file = new File(pathName);
                FileInputStream fis = new FileInputStream(file);
                DataOutputStream dos = new DataOutputStream(connection.getOutputStream());

                byte[] buffer = new byte[1024];
                int count;
                while ((count = fis.read(buffer)) != -1) {
                    dos.write(buffer, 0, count);
                }
                fis.close();
                dos.flush();
                dos.close();

                int responseCode = connection.getResponseCode();
                Log.d(TAG, "Status: " + responseCode);

                InputStream is = connection.getInputStream();
                byte[] responseBuffer = new byte[1024];
                StringBuilder responseStringBuilder = new StringBuilder();
                while ((count = is.read(responseBuffer)) != -1) {
                    responseStringBuilder.append(new String(responseBuffer, 0, count));
                }
                Log.d(TAG, responseStringBuilder.toString());

                return responseCode;
            } catch (IOException e) {
                e.printStackTrace();
                return -1;
            }
        }

        @Override
        protected void onPostExecute(Integer result) {
            if (result == 200) {
                Toast.makeText(SignUrlUploadActivity.this, "Upload successful.", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(SignUrlUploadActivity.this, "Upload failed.", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

Multipart upload

For large files, generate a presigned URL for each part and upload them independently. Each URL is signed separately.

package main

import (
	"bytes"
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)

var (
	region     string
	bucketName string
	objectName string
	length     = int64(5000 * 1024)       // Total file size in bytes.
	partSize   = int64(200 * 1024)        // Size of each part in bytes.
	partsNum   = int(length/partSize + 1) // Number of parts.
	data       = make([]byte, length)     // Simulated upload data.
)

func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	flag.Parse()

	if len(bucketName) == 0 || len(region) == 0 || len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name, region, and object name are required")
	}

	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	client := oss.NewClient(cfg)

	// Step 1: Initialize the multipart upload.
	initResult, err := client.InitiateMultipartUpload(context.TODO(), &oss.InitiateMultipartUploadRequest{
		Bucket: oss.Ptr(bucketName),
		Key:    oss.Ptr(objectName),
	})
	if err != nil {
		log.Fatalf("InitiateMultipartUpload failed: %v", err)
	}

	// Step 2: Generate a presigned URL for each part and upload it.
	for i := 0; i < partsNum; i++ {
		start := int64(i) * partSize
		end := start + partSize
		if end > length {
			end = length
		}

		signedResult, err := client.Presign(context.TODO(), &oss.UploadPartRequest{
			Bucket:     oss.Ptr(bucketName),
			Key:        oss.Ptr(objectName),
			PartNumber: int32(i + 1),
			Body:       bytes.NewReader(data[start:end]),
			UploadId:   initResult.UploadId,
		}, oss.PresignExpiration(time.Now().Add(1*time.Hour)))
		if err != nil {
			log.Fatalf("Presign for part %d failed: %v", i+1, err)
		}

		req, err := http.NewRequest(signedResult.Method, signedResult.URL, bytes.NewReader(data[start:end]))
		if err != nil {
			log.Fatalf("Failed to create HTTP request for part %d: %v", i+1, err)
		}

		c := &http.Client{}
		if _, err = c.Do(req); err != nil {
			log.Fatalf("Failed to upload part %d: %v", i+1, err)
		}
		fmt.Printf("Uploaded part %d\n", i+1)
	}

	// Step 3: List uploaded parts and collect their ETags.
	partsResult, err := client.ListParts(context.TODO(), &oss.ListPartsRequest{
		Bucket:   oss.Ptr(bucketName),
		Key:      oss.Ptr(objectName),
		UploadId: initResult.UploadId,
	})
	if err != nil {
		log.Fatalf("ListParts failed: %v", err)
	}

	var parts []oss.UploadPart
	for _, p := range partsResult.Parts {
		parts = append(parts, oss.UploadPart{PartNumber: p.PartNumber, ETag: p.ETag})
	}

	// Step 4: Complete the multipart upload.
	result, err := client.CompleteMultipartUpload(context.TODO(), &oss.CompleteMultipartUploadRequest{
		Bucket:   oss.Ptr(bucketName),
		Key:      oss.Ptr(objectName),
		UploadId: initResult.UploadId,
		CompleteMultipartUpload: &oss.CompleteMultipartUpload{
			Parts: parts,
		},
	})
	if err != nil {
		log.Fatalf("CompleteMultipartUpload failed: %v", err)
	}

	log.Printf("Multipart upload complete: %#v\n", result)
}

Upload with callback

Set upload callback parameters to receive a notification after a successful upload. The callback URL and body are Base64-encoded JSON passed as x-oss-callback and x-oss-callback-var headers in the presigned URL.

Step 1: Generate the presigned URL (object owner)

package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"flag"
	"log"
	"time"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)

var (
	region     string
	bucketName string
	objectName string
)

func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	flag.Parse()

	if len(bucketName) == 0 || len(region) == 0 || len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name, region, and object name are required")
	}

	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	client := oss.NewClient(cfg)

	// Define callback parameters.
	callbackMap := map[string]string{
		"callbackUrl":      "http://example.com:23450",
		"callbackBody":     "bucket=${bucket}&object=${object}&size=${size}&my_var_1=${x:my_var1}&my_var_2=${x:my_var2}",
		"callbackBodyType": "application/x-www-form-urlencoded",
	}
	callbackStr, err := json.Marshal(callbackMap)
	if err != nil {
		log.Fatalf("failed to marshal callback map: %v", err)
	}
	callbackBase64 := base64.StdEncoding.EncodeToString(callbackStr)

	callbackVarMap := map[string]string{
		"x:my_var1": "this is var 1",
		"x:my_var2": "this is var 2",
	}
	callbackVarStr, err := json.Marshal(callbackVarMap)
	if err != nil {
		log.Fatalf("failed to marshal callback var: %v", err)
	}
	callbackVarBase64 := base64.StdEncoding.EncodeToString(callbackVarStr)

	// Generate a presigned URL with callback parameters embedded.
	result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
		Bucket:      oss.Ptr(bucketName),
		Key:         oss.Ptr(objectName),
		Callback:    oss.Ptr(callbackBase64),
		CallbackVar: oss.Ptr(callbackVarBase64),
	},
		oss.PresignExpires(10*time.Minute),
	)
	if err != nil {
		log.Fatalf("failed to presign PutObject: %v", err)
	}

	log.Printf("Method: %v\n", result.Method)
	log.Printf("Expiration: %v\n", result.Expiration)
	log.Printf("URL: %v\n", result.URL)

	if len(result.SignedHeaders) > 0 {
		log.Printf("Signed headers (must be sent by the uploader):\n")
		for k, v := range result.SignedHeaders {
			log.Printf("  %v: %v\n", k, v)
		}
	}
}

Step 2: Upload using the presigned URL (third party)

The uploader must include the x-oss-callback and x-oss-callback-var headers, which are embedded in result.SignedHeaders.

curl

curl -X PUT \
     -H "x-oss-callback: eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9" \
     -H "x-oss-callback-var: eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==" \
     -T "/path/to/local/file" \
     "<signedUrl>"

Python

import requests

def upload_file(signed_url, file_path, headers=None):
    if not headers:
        headers = {}
    try:
        with open(file_path, 'rb') as file:
            response = requests.put(signed_url, data=file, headers=headers)
        print(f"Status: {response.status_code}")
        print(response.text)
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    signed_url = "<signedUrl>"
    file_path = "/path/to/local/file"
    headers = {
        "x-oss-callback": "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9",
        "x-oss-callback-var": "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==",
    }
    upload_file(signed_url, file_path, headers)

Go

package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func uploadFile(signedUrl, filePath string, headers map[string]string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	fileBytes, err := io.ReadAll(file)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("PUT", signedUrl, bytes.NewBuffer(fileBytes))
	if err != nil {
		return err
	}

	for key, value := range headers {
		req.Header.Add(key, value)
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	fmt.Printf("Status: %d\n", resp.StatusCode)
	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
	return nil
}

func main() {
	signedUrl := "<signedUrl>"
	filePath := "/path/to/local/file"
	headers := map[string]string{
		"x-oss-callback":     "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9",
		"x-oss-callback-var": "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==",
	}

	if err := uploadFile(signedUrl, filePath, headers); err != nil {
		fmt.Printf("Error: %v\n", err)
	}
}

Java

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;
import java.util.*;

public class SignUrlUpload {
    public static void main(String[] args) throws Throwable {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;

        URL signedUrl = new URL("<signedUrl>");
        String pathName = "/path/to/local/file";

        Map<String, String> headers = new HashMap<>();
        headers.put("x-oss-callback", "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9");
        headers.put("x-oss-callback-var", "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==");

        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);

            for (Map.Entry<String, String> header : headers.entrySet()) {
                put.addHeader(header.getKey(), header.getValue());
            }

            httpClient = HttpClients.createDefault();
            response = httpClient.execute(put);
            System.out.println("Status: " + response.getStatusLine().getStatusCode());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
            httpClient.close();
        }
    }
}

PHP

<?php

function uploadFile($signedUrl, $filePath, $headers = []) {
    if (!file_exists($filePath)) {
        echo "File not found: $filePath\n";
        return;
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $signedUrl);
    curl_setopt($ch, CURLOPT_PUT, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_INFILE, fopen($filePath, 'rb'));
    curl_setopt($ch, CURLOPT_INFILESIZE, filesize($filePath));
    curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(function($key, $value) {
        return "$key: $value";
    }, array_keys($headers), $headers));

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    echo "Status: $httpCode\n";
    echo $response . "\n";
}

$signedUrl = "<signedUrl>";
$filePath = "/path/to/local/file";
$headers = [
    "x-oss-callback"     => "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9",
    "x-oss-callback-var" => "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==",
];

uploadFile($signedUrl, $filePath, $headers);

Troubleshooting

SignatureDoesNotMatch (403)

This error means OSS rejected the signature. Check the following in order:

  1. Signed headers not sent by the uploader. Check result.SignedHeaders — any header listed there must be sent with the exact same value in the PUT request. This is the most common cause.

  2. Content-Type mismatch. If ContentType was set when generating the URL, the uploader must send the same Content-Type header. Browsers add Content-Type automatically, which can cause a mismatch if it was not included at presign time.

  3. URL modified after generation. Use the URL exactly as returned. Do not encode, decode, or alter the query string.

  4. URL expired. Check result.Expiration. If the URL has expired, generate a new one.

  5. System clock skew. Signature V4 is time-sensitive. Make sure your server clock is synchronized (for example, using NTP). Excessive clock skew causes signature validation to fail.

  6. Wrong region. The URL is region-specific. Confirm that the region in your client configuration matches the bucket's actual region.

URL works from curl but not from a browser

Browsers automatically add headers (especially Content-Type) that were not present when the URL was generated. Regenerate the presigned URL with ContentType explicitly set to the value the browser will send.

Upload succeeds but callback is not triggered

  • Verify the callback URL is publicly reachable from the internet.

  • Check that the Base64-encoded JSON is correctly formed — decode it to confirm the structure before embedding.

References