DEV Community

Cover image for Conquering the Reddit API: How to Natively Upload Images via Node.js (And Survive the S3 Boss Fight)
freerave
freerave

Posted on

Conquering the Reddit API: How to Natively Upload Images via Node.js (And Survive the S3 Boss Fight)

A step-by-step guide to defeating the undocumented traps of Reddit's native image upload API, avoiding AWS S3 400 errors, and the infamous Invalid image URL.
If you've ever tried building a tool to auto-publish posts to Reddit using their API, you've probably hit a wall when it comes to images.

Sure, you could just upload your images to Imgur and post a link, but we want that sweet, native, inline Reddit image experience! The official Reddit API does have an endpoint for this (/api/media/asset.json), but trying to use it in Node.js feels like walking through a minefield blindfolded.

After hours of debugging, screaming at my terminal, and fighting AWS S3, I finally cracked the exact sequence and undocumented gotchas required to make this work.

If you are seeing 400 Bad Request from S3, Bucket POST must contain a field named 'key', or Reddit mocking you with Invalid image URL... grab a coffee. You're in the right place. ☕


🛠 Prerequisites: The Setup

Before we start the dance, make sure you have your dependencies ready. For this guide, we'll use axios for HTTP requests and form-data to build our multipart payloads.

import axios from 'axios';
import fs from 'fs';
import path from 'path';
import FormData from 'form-data';

// You should already have this from your Reddit OAuth completion
const REDDIT_ACCESS_TOKEN = 'your_oauth_access_token';
Enter fullscreen mode Exit fullscreen mode

The Master Plan

To upload a native image to Reddit, you don't just send an image to a Reddit server. It's a 3-step dance:

  1. Get the Lease: Ask Reddit for an AWS S3 upload lease (/api/media/asset.json).
  2. S3 Upload: Upload the file directly to AWS S3 using the lease details.
  3. Publish: Tell Reddit to create the post (/api/submit), pointing it to the correct S3 location.

Sounds simple? Let's look at the traps.


Step 1: Getting the Lease

First, you tell Reddit you want to upload a file.

const mediaFile = 'path/to/my-awesome-meme.png';
const ext = path.extname(mediaFile).toLowerCase();

const params = new URLSearchParams();
params.append('filepath', path.basename(mediaFile));
params.append('mimetype', 'image/png'); // Important: send correct mimetype

const leaseResponse = await axios.post('https://oauth.reddit.com/api/media/asset.json', params, {
    headers: { 'Authorization': `Bearer ${REDDIT_ACCESS_TOKEN}` }
});

const { args, asset } = leaseResponse.data;
if (!args || !asset?.asset_id) {
    throw new Error('Failed to get upload lease from Reddit');
}
Enter fullscreen mode Exit fullscreen mode

Reddit responds with an asset_id and args, which contains the AWS action URL (usually https://reddit-uploaded-media.s3-accelerate.amazonaws.com) and the fields required to authorize the S3 upload.


Step 2: The S3 Upload (Where Dreams go to Die)

🚨 Trap #1: The args.fields Array Trap

If you look at the args.fields returned by Reddit, its structure looks something like this:

"fields": [
  { "name": "key", "value": "reddit-uploaded-media/xyz123" },
  { "name": "AWSAccessKeyId", "value": "..." },
  { "name": "policy", "value": "..." }
]
Enter fullscreen mode Exit fullscreen mode

Notice anything? It's an array of objects, not a standard key-value dictionary!
If you try to map over it using standard dictionary parsers like Object.entries(), Node.js will append the fields with numeric keys like "0", "1", "2".

AWS S3 will ruthlessly reject your upload with an XML error message:
<Message>Bucket POST must contain a field named 'key'</Message>

The Fix: Parse it explicitly as an array to correctly populate FormData.

const formData = new FormData();
let s3Key = '';

// Crucial: Handle array structure returned by Reddit!
if (Array.isArray(args.fields)) {
    args.fields.forEach((field) => {
        formData.append(field.name, String(field.value));
        if (field.name === 'key') s3Key = String(field.value); // Save this!
    });
}
Enter fullscreen mode Exit fullscreen mode

🚨 Trap #2: The Chunked Encoding Trap

We need to append our actual image file to the FormData. However, if you are passing a stream (e.g., fs.createReadStream()), Node.js HTTP clients (like axios) will default to sending the request via Transfer-Encoding: chunked.

AWS S3 strictly forbids chunked POST requests for presigned URLs and will instantly strike you down with a 400 Bad Request.

The Fix: You must append the file with explicit content types AND calculate the exact Content-Length before firing the request.

// Ensure file is the LAST field added to formData!
formData.append('file', fs.createReadStream(mediaFile), { 
    filename: path.basename(mediaFile),
    contentType: 'image/png' 
});

// Calculate length to avoid S3 chunked transfer encoding rejection
const contentLength = await new Promise((resolve, reject) => {
    formData.getLength((err, length) => err ? reject(err) : resolve(length));
});

// Reddit's action URL might be missing the protocol
const uploadUrl = args.action.startsWith('http') ? args.action : `https:${args.action}`;

await axios.post(uploadUrl, formData, {
    headers: {
        ...formData.getHeaders(),
        'Content-Length': contentLength.toString()
    },
    timeout: 60000,
    maxBodyLength: Infinity,
    maxContentLength: Infinity
});

console.log('✅ Successfully uploaded to S3!');
Enter fullscreen mode Exit fullscreen mode

S3 should now return a beautiful 201 Created or 204 No Content response.


Step 3: Publishing the Post (The Final Boss)

Now that the image is securely on S3, we need to create the actual Reddit post.

🚨 Trap #3: The Invalid image URL Trap

You might logically think: "The image is uploaded, so it will be hosted on Reddit's CDN! I'll just pass url: https://i.redd.it/{asset_id}.png in my publish payload!"

WRONG.
If you try to publish using the i.redd.it URL, the Reddit API will immediately attempt to fetch it to validate your submission. Since the CDN hasn't populated your file globally yet, Reddit's backend gets a 404 underneath, and throws an Invalid image URL error directly in your face.

The Fix: Give Reddit the exact, raw AWS S3 URL you just pushed the image to! That's the URL constructed by joining the uploadUrl and the s3Key we saved earlier.

// e.g., https://reddit-uploaded-media.s3-accelerate.amazonaws.com/reddit-uploaded-media/xyz123
const finalS3Url = `${uploadUrl}/${s3Key}`; 

const postData = new URLSearchParams();
postData.append('api_type', 'json');
postData.append('sr', 'YourTargetSubredditName'); 
postData.append('title', 'My Awesome Native Image Post');
postData.append('kind', 'image'); // Tell Reddit it's a native media post
postData.append('url', finalS3Url); // Crucial: Give them the raw S3 link!

// Pro-tip: If you are testing this code repeatedly with the same 
// image/title, Reddit will block it as a duplicate. Use 'resubmit'.
postData.append('resubmit', 'true'); 

const publishResponse = await axios.post('https://oauth.reddit.com/api/submit', postData, {
    headers: { 'Authorization': `Bearer ${REDDIT_ACCESS_TOKEN}` }
});

// Check if Reddit API returned logic errors inside the 200 OK!
if (publishResponse.data.json?.errors?.length > 0) {
    console.error('Reddit API Errors:', publishResponse.data.json.errors);
} else {
    console.log('🎉 Boom! Post successful:', publishResponse.data.json?.data?.name);
}
Enter fullscreen mode Exit fullscreen mode

Boom! 🚀

Reddit will take that S3 URL, ingest it, process it, and magically swap it out for a native i.redd.it link on the platform. Your post will now show up with a beautiful, inline image, no third-party URLs or external thumbnails required.

If this saved you from pulling the rest of your hair out, share it with a fellow developer. Happy coding, and may your S3 buckets forever be forgiving!

Top comments (0)