When uploading files directly from web browsers or mobile apps to Nyota Drive using S3 pre-signed URLs, developers occasionally encounter 403 Forbidden errors with error codes such as SignatureDoesNotMatch, Request has expired, or RequestTimeTooSkewed.
This guide explains how AWS Signature Version 4 (SigV4) calculation works under the hood, why mismatches occur, and how to format your upload requests for 100% reliable direct S3 uploads.
1. How Pre-Signed Uploads Work Under the Hood
To avoid routing heavy video or image binaries through your application server, Nyota Drive uses a 2-Phase Direct-to-Storage Pattern:
1. PHASE 1: RESERVATION (Your Backend Server -> Nyota API)
Backend requests presigned URL: POST /v1/drive/files/upload/request
Payload: { fileName: "photo.png", mimeType: "image/png", sizeBytes: 1048576 }
-> Nyota computes AWS SigV4 signature bound to "image/png"
-> Returns: { uploadUrl: "https://drive.nyotaimara.com/...", reservationId: "..." }
│
▼
2. PHASE 2: DIRECT BINARY STREAM (Browser -> S3 Storage Node)
Browser streams raw file bytes via HTTP PUT directly to the storage server:
PUT https://drive.nyotaimara.com/bucket/photo.png
Header: Content-Type: image/png
│
▼
3. SIGNATURE EVALUATION
S3 server recalculates the SigV4 hash of incoming headers against the URL signature.
- Headers match exactly -> 200 OK (Upload Success)
- Headers differ -> 403 SignatureDoesNotMatch (Rejected)2. The 4 Common Causes of S3 Signature Errors
Error Code | Root Cause | Immediate Fix |
| Header Mismatch: The | Ensure the client |
| Timeout: The upload started or completed more than 60 minutes after the pre-signed URL was generated. | Generate a fresh upload reservation URL if the upload was paused or delayed. |
| Clock Drift: The client device or server clock differs by more than 15 minutes from standard UTC time. | Synchronize client system time with NTP ( |
3. Step-by-Step Code Fixes
Fix A: Matching Content-Type Headers in Browser Uploads
The most common mistake is letting Axios or fetch automatically set Content-Type: multipart/form-data or application/octet-stream when the pre-signed URL was computed for image/png.
Correct React / TypeScript Browser Upload Implementation:
// browser-upload.ts
async function uploadFileToNyotaDrive(file: File) {
// Step 1: Request upload reservation from your backend API
const reservationRes = await fetch("/api/upload-reservation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fileName: file.name,
mimeType: file.type, // e.g. "image/png"
sizeBytes: file.size,
}),
});
const { uploadUrl, reservationId } = await reservationRes.json();
// Step 2: Upload raw file bytes directly to Nyota Drive via HTTP PUT
// CRITICAL: The Content-Type header MUST match file.type exactly!
const uploadRes = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": file.type, // <--- MUST MATCH RESERVATION
},
body: file, // Stream raw binary File object (NOT FormData!)
});
if (!uploadRes.ok) {
const errorText = await uploadRes.text();
throw new Error(`S3 Upload failed with status ${uploadRes.status}: ${errorText}`);
}
// Step 3: Commit the completed upload
await fetch("/api/upload-commit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reservationId }),
});
console.log("File uploaded and committed successfully!");
}4. Troubleshooting Checklist
Before opening a technical support ticket, verify these four items:
- Is your HTTP request using the PUT method (not POST)?
- Does the Content-Type header match the exact MIME type specified during reservation?
- Are you uploading the raw binary buffer or File object (rather than wrapping it inside new FormData())?
- Was the upload completed within 60 minutes of URL generation?