Upload to AWS S3 Using Axios Pre-Signed URLs

Uploading files directly to Amazon S3 via pre-signed URLs removes load from your application server while keeping S3 credentials secure. This guide covers how to properly configure the Axios HTTP client to execute PUT requests against S3 pre-signed upload URLs, manage upload progress, handle required headers, and bypass common issues like authorization conflicts and signature mismatches.

The Core Upload Flow

When an S3 pre-signed upload URL is generated on the server, it is bound to a specific HTTP method (almost always PUT), a destination path, and optionally specific headers such as Content-Type.

To complete the upload, Axios must send a PUT request with the exact binary/blob data and matching headers.

Basic Browser Configuration

In browser environments, you pass the native File or Blob object directly into the data field of an Axios PUT request.

import axios from 'axios';

async function uploadFileToS3(presignedUrl, file) {
  try {
    const response = await axios.put(presignedUrl, file, {
      headers: {
        'Content-Type': file.type || 'application/octet-stream',
      },
      onUploadProgress: (progressEvent) => {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        );
        console.log(`Upload progress: ${percentCompleted}%`);
      },
    });

    return response.status === 200;
  } catch (error) {
    console.error('Error uploading file to S3:', error);
    throw error;
  }
}

Node.js Configuration

For Node.js environments, use a Buffer or a readable stream with fs.createReadStream().

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

async function uploadFileStream(presignedUrl, filePath, mimeType) {
  const fileStream = fs.createReadStream(filePath);
  const fileStats = fs.statSync(filePath);

  return axios.put(presignedUrl, fileStream, {
    headers: {
      'Content-Type': mimeType,
      'Content-Length': fileStats.size,
    },
    maxBodyLength: Infinity,
    maxContentLength: Infinity,
  });
}

Critical Axios Configuration Rules

1. Match Headers Exactly

If the pre-signed URL was generated specifying a ContentType or server-side encryption header (e.g., x-amz-server-side-encryption), those exact headers must be included in the Axios configuration. Conversely, do not add headers that were not part of the signed signature, as S3 will reject the request with a 403 Forbidden (SignatureDoesNotMatch) error.

2. Avoid Global Interceptors

If your application uses global Axios interceptors that attach Authorization: Bearer <token> or other API headers to every request, S3 will reject the upload. Use an isolated Axios instance for S3 operations:

// Create a dedicated instance without app-specific interceptors
const s3Client = axios.create();

// Ensure default headers are stripped if inherited
delete s3Client.defaults.headers.common['Authorization'];

await s3Client.put(presignedUrl, file, {
  headers: {
    'Content-Type': file.type,
  },
});

3. Handle Large File Limits in Node.js

By default, Axios limits request body sizes in Node.js environments. When uploading large files, explicitly set maxBodyLength and maxContentLength to Infinity in your request configuration.

4. Enable S3 CORS

Ensure your AWS S3 bucket has a Cross-Origin Resource Sharing (CORS) policy configured to allow PUT requests and expose needed headers for browser-based uploads:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT"],
    "AllowedOrigins": ["https://yourdomain.com"],
    "ExposeHeaders": ["ETag"]
  }
]