404 Error on Image upload

Uploading posts via API is working just fine, but I am new to uploading images.

Any idea what I’m doing wrong? (404 error)

// Import the necessary modules
import axios from 'axios';
import fs from 'fs';
import FormData from 'form-data';
import dotenv from 'dotenv';
import GhostAdminAPI from '@tryghost/admin-api';


// Initialize dotenv
dotenv.config();

// Define Ghost Admin API key and URL
const GHOST_ADMIN_API_KEY = process.env.GHOST_ADMIN_API_KEY;
const GHOST_ADMIN_URL = process.env.GHOST_ADMIN_URL;

// Ensure all necessary environment variables are set
if (!GHOST_ADMIN_API_KEY || !GHOST_ADMIN_URL) {
    console.error('ERROR: Missing necessary environment variables. Please set GHOST_ADMIN_API_KEY and GHOST_ADMIN_URL.');
    process.exit(1);
}

// Function to upload the image
const uploadImageToGhost = async (imagePath) => {
    try {
        // Check if the file exists
        if (!fs.existsSync(imagePath)) {
            console.error('ERROR: The specified image file does not exist:', imagePath);
            return;
        }

        // Create a new instance of FormData
        const formData = new FormData();

        // Add the image file to the form data
        formData.append('file', fs.createReadStream(imagePath));

        // Axios configuration for the POST request
        const config = {
            headers: {
                'Authorization': `Ghost ${GHOST_ADMIN_API_KEY}`,
                ...formData.getHeaders()  // Spread operator to merge form data headers
            }
        };

        // Send POST request to Ghost API
        const response = await axios.post(`${GHOST_ADMIN_URL}/images/upload/`, formData, config);

        // Log the URL of the uploaded image
        console.log('Image uploaded successfully. Image URL:', response.data.url);
    } catch (error) {
        console.error('An error occurred while uploading the image:', error);
    }
}

// Call the function
uploadImageToGhost("./inputData/georgia.jpg");

A 404 error usually means that the resource you’re trying to reach can’t be found.

My best guess, based on the code, would be that GHOST_ADMIN_URL isn’t resolving to the correct path.

Are you sure the endpoint you’re calling is actually https://example.com/ghost/api/admin/images/upload/?

I have console logged and confirmed

https://myurl.ghost.io/ghost/api/admin/images/upload/

is the URL being passed in, still debugging…

It seems there is a JWT token error now (or always?). If anyone has a generic example of a node.js script that simply uploads one image to Ghost successfully, I would love to see it. I may be defeated on this one.

Random question, is this simply just not handled via the API?

Since you’re a Ghost(Pro) customer, you can contact support directly using the email found in the footer.

It looks like you’re using the SDK, which means that uploading an image should be as simple as this:

const GhostAdminAPI = require("@tryghost/admin-api");
const api = new GhostAdminAPI({
    url: process.env.GHOST_ADMIN_API_URL,
    key: process.env.GHOST_ADMIN_API_KEY,
    version: "v5.0"
});

async function uploadImage() {
  try {
    const res = await api.images.upload({file: "path/to/image"});
    console.log(res); 
   /* expected output: {
        url: 'path to image on Ghost',
        ref: null
        } 
   */
   } catch (e) {
      console.log(e) 
   }
}

uploadImage();
1 Like

It might be an authentication issue then, not a 404 error.

On that front, I see that you’re trying to use the GHOST_ADMIN_API_KEY directly in the Authorization header. However, you first need to create a JWT token from it, as outlined here.

I have modified your script a bit and tested it with one of my Ghost instances. It is working now :slight_smile:

// Import the necessary modules
import axios from 'axios';
import fs from 'fs';
import FormData from 'form-data';
import dotenv from 'dotenv';
import GhostAdminAPI from '@tryghost/admin-api';

// Initialize dotenv
dotenv.config();

// Define Ghost Admin API key and URL
const GHOST_ADMIN_API_KEY = process.env.GHOST_ADMIN_API_KEY;
const GHOST_ADMIN_URL = process.env.GHOST_ADMIN_URL;

// Ensure all necessary environment variables are set
if (!GHOST_ADMIN_API_KEY || !GHOST_ADMIN_URL) {
  console.error(
    'ERROR: Missing necessary environment variables. Please set GHOST_ADMIN_API_KEY and GHOST_ADMIN_URL.'
  );
  process.exit(1);
}

// Split the key into ID and SECRET
const [id, secret] = GHOST_ADMIN_API_KEY.split(':');

// Create the token (including decoding secret)
const token = jwt.sign({}, Buffer.from(secret, 'hex'), {
  keyid: id,
  algorithm: 'HS256',
  expiresIn: '5m',
  audience: `/admin/`,
});

// Function to upload the image
const uploadImageToGhost = async (imagePath) => {
  try {
    // Check if the file exists
    if (!fs.existsSync(imagePath)) {
      console.error(
        'ERROR: The specified image file does not exist:',
        imagePath
      );
      return;
    }

    // Create a new instance of FormData
    const formData = new FormData();

    // Add the image file to the form data
    formData.append('file', fs.createReadStream(imagePath));

    // Axios configuration for the POST request
    const config = {
      headers: {
        Authorization: `Ghost ${token}`,
        ...formData.getHeaders(), // Spread operator to merge form data headers
      },
    };
    // Send POST request to Ghost API
    const response = await axios.post(
      `${GHOST_ADMIN_URL}/images/upload/`,
      formData,
      config
    );

    // Log the URL of the uploaded image
    console.log(
      'Image uploaded successfully. Image URL:',
      response.data.images[0].url
    );
  } catch (error) {
    console.error(
      'An error occurred while uploading the image:',
      error.response.data
    );
  }
};

uploadImageToGhost('./inputData/georgia.jpg');

Another note on it: based on this script you actually don’t need the @tryghost/admin-api module. You could, however, re-write the script to use it – might make things a bit easier in regards to the token generation (since that is handled for you), but add another dependency.

EDIT: @RyanF provided a great example on how to do this with the SDK – now you have two versions and can choose which one you prefer :smiley:

1 Like