I have a Netlify function which works fine in local development (using netlify dev), but in production an Internal Error: 500, Resource not found is thrown in the console, seemingly failing to find the uploadProductImage Netlify function. I’ve included it below, but given that it never even gets run (log lines/return new Response(“ok”) at the very beginning of the function are not called, this is not a runtime error) I don’t know if it’ll be all that useful.
// netlify/functions/uploadProductImage.mts
/**
* Handles uploading an image to a Supabase bucket. These images
* are compressed marginally to prevent huge file sizes but are otherwise left
* untouched.
*/
import {Context} from '@netlify/functions';
import formidable from 'formidable';
import sharp from 'sharp';
import fs from 'fs/promises';
import {Readable} from 'stream';
import {formatBytes} from '../lib/lib.mts';
import getSupabaseClient from "../lib/getSupabaseClient.mts";
import {SupabaseClient} from '@supabase/supabase-js';
/** The target maximum size for the image in bytes */
const TARGET_IMAGE_SIZE = 1000 * 1000;
export default async function handler(request: Request, _context: Context) {
if (request.method !== 'POST') return new Response('Method Not Allowed', {status: 405});
const authHeader = request.headers.get("Authorization") ?? undefined;
if (!authHeader) return new Response("No Authorization supplied", {status: 403})
let supabase: SupabaseClient
try {
supabase = await getSupabaseClient(authHeader);
} catch (e: any) {
return new Response(e.message, {status: e.status})
}
// Convert body -> Buffer
const arrayBuffer = await request.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// Wrap it in a Readable so formidable can parse it
const req = bufferToReadable(buffer) as any;
req.headers = Object.fromEntries((request.headers as any).entries());
const form = formidable({multiples: false});
console.log('Content-Type header:', req.headers['content-type']); // TODO: Remove Debug Line
// Use Promise to await the callback from form.parse,
// resulting in the fields and files from the form.
let {fields, files}: {fields: any, files: any} = {fields: undefined, files: undefined};
try {
const parsed = await new Promise<{ fields: any; files: any }>(
(resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) reject(err);
else resolve({fields, files});
}
)
}
);
fields = parsed.fields;
files = parsed.files;
} catch (error) {
console.error('Formidable parse failed:', error);
return new Response('Failed to parse form data', { status: 400 });
}
const uploaded = files.file?.[0];
if (!uploaded) {
return new Response('No file uploaded.', {status: 400});
}
// Read the temp file path to buffer
const uncompressedBuffer: Buffer = await fs.readFile(uploaded.filepath);
const fileName: string = uploaded.originalFilename;
// Compress
console.log("Compressing image...")
const compressedBuffer: Buffer = await convertAndCompressToWebp(uncompressedBuffer, TARGET_IMAGE_SIZE);
// Upload File to Supabase
// TODO: Handle duplicate names
const {error} = await supabase!.storage
.from(fields.bucket)
.upload(fileName, compressedBuffer, {
contentType: "image/webp",
});
if (error && error.message === "The resource already exists") {
console.warn(`Duplicate image name upload attempted: ${fileName}`);
//return new Response(`An image with the name "${fileName}" already exists. Please rename the file and try again.`, { status: 409 });
} else if (error) {
console.error(`Error while uploading new image: ${error}`);
return new Response(JSON.stringify(error), {status: 500});
}
// Then fetch its ID and URL
const {data: idData, error: idError} = await supabase!
.from("objects")
.select(`id`)
.eq("name", fileName)
if (idError) {
console.error(`Error while fetching image ID after saving a new image: ${idError.message}`)
return new Response("Failed to fetch image ID after saving", {status: 500});
}
const fileID = idData[0].id
const fileURL = supabase!.storage.from(fields.bucket).getPublicUrl(fileName).data.publicUrl
return new Response(
JSON.stringify({
fileName: uploaded.originalFilename,
size: uploaded.size,
fileID,
fileURL
}),
{status: 200, headers: {"Content-Type": "application/json"}}
);
}
/**
* Convert a buffer to a Readable object
* @param buffer The Buffer to convert to Readable
* @returns A Readable object containing the contents of the buffer
*/
function bufferToReadable(buffer: Buffer): Readable {
return new Readable({
read() {
this.push(buffer);
this.push(null);
},
});
}
/**
* Compress an image buffer below the target maximum size
* @param buffer The image buffer to compress
* @param targetMax Target max image size in bytes
* @returns
*/
async function convertAndCompressToWebp(buffer: Buffer, targetMax: number): Promise<Buffer> {
let quality = 100; // Start quality
let resizeBuffer = await sharp(buffer).resize({width: 750}).toBuffer()
let outputBuffer = await sharp(resizeBuffer).webp({quality}).toBuffer();
// Reduce quality until size fits or minimum quality reached
while (outputBuffer.length > targetMax && quality > 1) {
quality -= 10;
console.log("Attempting compression quality: " + quality)
if (quality <= 0) {
quality = 1;
}
outputBuffer = await sharp(resizeBuffer).webp({
quality,
smartSubsample: true,
smartDeblock: true,
effort: 6,
}).toBuffer();
}
console.log(`Image compressed with quality ${quality}, file size ${formatBytes(outputBuffer.length)}`);
return outputBuffer;
}
This is the client side code that’s called when running the Netlify function
// src/lib/netlifyFunctions.tsx
export async function fetchFromNetlifyFunction(
func: string,
body?: string | ArrayBuffer | Blob | File | FormData | URLSearchParams | ReadableStream,
jwt?: Promise<string | undefined>
): Promise<{data?: any, error?: any}> {
const endpoint: string = window.location.origin + "/.netlify/functions/" + func
const jwtString = await jwt
const headers = jwtString ? {Authorization: `Bearer ${jwtString}`} : undefined
try {
const response = await (body
? fetch(endpoint, { headers, method: "POST", body })
: fetch(endpoint, { headers })
)
const responseBody = await response.text()
if (!response.ok) {
throw new Error(responseBody)
}
return {data: softParseJSON(responseBody)}
} catch (error: any) {
console.error(error)
return {error}
}
}
/**
* Uploads an image to the given bucket.
* @param image The image file to upload.
* @param bucket The name of the bucket to upload the image to.
* @returns An object with 4 keys:
* * `fileName` - The name of the file in the bucket.
* * `size` - The size of the file in the bucket.
* * `fileID` - The ID of the created file object.
* * `fileURL` - The URL where the image can be accessed.
*/
export async function uploadImage(image: File, bucket: string): Promise<{
fileName: string
size: number
fileID: string
fileURL: string
}> {
const formData = new FormData()
formData.append("file", image)
formData.append("bucket", bucket)
const {data, error} = await fetchFromNetlifyFunction("uploadProductImage", formData, getJWTToken())
console.log(data)
if (error) throw error
return data
}
The issue is not to do with my compilation or build, because everything else (including a bunch of other Netlify functions) work fine, it’s just this one that causes the error.
The site name is thiswebsiteissogay.netlify.app, but I’m using a custom domain: thisshopissogay.com.
You won’t be able to access the dashboard where the functionality for uploading product images is, so you won’t be able to run the function yourself to test, but this is the ID of one of the failed requests: 01K9SKS8QE3D93KE38BJMS1N2X.
I tried sending a request to the endpoint using Postman and got back a 400: No file uploaded error, which means the function was found and ran as expected, but when triggered actually on the website, using the code above, I get the 500 error.
I have tried Ask Netlify, ChatGPT, and looking for posts and support online myself, but have found nothing. The main thing suggested by these places was case sensitivity, but as you can confirm yourself in the code above, this is all correct.