skip to Main Content

I’ve got a weird issue with my nextjs app on a ubuntu box. I’m using nginx. I have the uploads folder in the nginx config for the site:

location /uploads {
        alias /home/ubuntu/staging/thomson-print-portal/current/public/uploads;
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

The files upload correctly:

total 17296
drwxrwxr-x 2 ubuntu ubuntu    4096 Aug 29 16:10 ./
drwxrwxr-x 3 ubuntu ubuntu    4096 Aug 24 18:52 ../
-rw-rw-r-- 1 ubuntu ubuntu  546325 Aug 24 19:38 1724528290711.png
-rw-rw-r-- 1 ubuntu ubuntu 3609033 Aug 24 19:38 1724528322595.png
-rw-rw-r-- 1 ubuntu ubuntu 1231890 Aug 24 19:56 1724529410208.png
-rw-rw-r-- 1 ubuntu ubuntu  369096 Aug 24 20:00 1724529613975.pdf
-rw-rw-r-- 1 ubuntu ubuntu 3609033 Aug 27 20:25 1724790319429.png
-rw-rw-r-- 1 ubuntu ubuntu 3609033 Aug 27 20:27 1724790479629.png
-rw-r--r-- 1 ubuntu ubuntu 3609033 Aug 29 15:41 1724946095568.png
-rw-r--r-- 1 ubuntu ubuntu  546325 Aug 29 15:44 1724946251056.png
-rw-r--r-- 1 ubuntu ubuntu  546325 Aug 29 16:10 1724947844252.png

When the request for the image is made, it returns a 400 error:

GET https://print-portal.1905newmedia.com/_next/image?url=%2Fuploads%2F1724947844252.png&w=256&q=75 400 (Bad Request)

this is the component that shows the images:

// ~/src/app/_components/shared/artworkComponent/artworkComponent.tsx
// Accepts props: artworkUrl, artworkDescription
// Conditionally renders an image with the given artworkUrl and description depending on the file type of the artworkUrl
// Renders File Type Icon and Description if the file type is not an image

// Props:
// - artworkUrl: string
// - artworkDescription: string | null

// Usage:
// <ArtworkComponent artworkUrl={artwork.fileUrl} artworkDescription={artwork.description} />

import React from "react";
import Image from "next/image";

type ArtworkComponentProps = {
    artworkUrl: string;
    artworkDescription: string | null;
};

const ArtworkComponent: React.FC<ArtworkComponentProps> = ({
    artworkUrl,
    artworkDescription,
}) => {
    const isImage = artworkUrl.match(/.(jpeg|jpg|gif|png)$/) != null;
    const isPdf = artworkUrl.match(/.(pdf)$/) != null;
    const isPSD = artworkUrl.match(/.(psd)$/) != null;

    return (
        <div>
            {isImage && (
                <Image src={artworkUrl ? artworkUrl : ''} alt={artworkDescription ? artworkDescription : ''} width={200} height={200} />
            )}
            {isPdf && (
                <div>
                    {/* Show SVG Image for PDF */}
                    <svg className="h-10 w-10 text-red-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">  <path stroke="none" d="M0 0h24v24H0z" />  <rect width="6" height="6" x="14" y="5" rx="1" />  <line x1="4" y1="7" x2="10" y2="7" />  <line x1="4" y1="11" x2="10" y2="11" />  <line x1="4" y1="15" x2="20" y2="15" />  <line x1="4" y1="19" x2="20" y2="19" /></svg>
                </div>
            )}
            {isPSD && (
                <div>
                    <svg className="h-10 w-10 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">  <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />  <circle cx="8.5" cy="8.5" r="1.5" />  <polyline points="21 15 16 10 5 21" /></svg>
                </div>
            )}
            <p><strong>Artwork: </strong>{artworkUrl ? artworkUrl : ''}</p>
            <p><strong>Description: </strong>{artworkDescription ? artworkDescription : ''}</p>
            {/* Show File Type */}
            <p><strong>File Type: </strong>{isImage ? 'Image' : isPdf ? 'PDF' : isPSD ? 'PSD' : 'Unknown'}</p>
        </div>
    );
};
export default ArtworkComponent;

This is the component that handles uploading the images:

// ~/src/app/_components/shared/fileUpload.tsx
import React, { useState, useEffect, useRef } from 'react';
import Image from 'next/image';

interface FileUploadProps {
    onFileUploaded: (fileUrl: string, description: string) => void;
    onFileRemoved: (fileUrl: string) => void;
    onDescriptionChanged: (fileUrl: string, description: string) => void;
    workOrderItemId?: string | null;
    initialFiles?: { fileUrl: string; description: string }[];
}

const FileUpload: React.FC<FileUploadProps> = ({
    onFileUploaded,
    onFileRemoved,
    onDescriptionChanged,
    workOrderItemId,
    initialFiles = []
}) => {
    const fileInputRef = useRef<HTMLInputElement>(null);
    const [uploadedFiles, setUploadedFiles] = useState<{ fileUrl: string; description: string }[]>(initialFiles);
    const [uploading, setUploading] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const [uploadProgress, setUploadProgress] = useState(0);

    const allowedFileTypes = ['image/png', 'image/jpeg', 'application/pdf', 'image/vnd.adobe.photoshop'];
    const allowedExtensions = ['.png', '.jpg', '.jpeg', '.pdf', '.psd'];

    useEffect(() => {
        return () => {
            uploadedFiles.forEach(file => {
                if (file.fileUrl.startsWith('blob:')) {
                    URL.revokeObjectURL(file.fileUrl);
                }
            });
        };
    }, [uploadedFiles]);

    useEffect(() => {
        setUploadedFiles(initialFiles);
    }, [initialFiles]);

    const validateFile = (file: File): boolean => {
        const fileType = file.type;
        const fileExtension = `.${file.name.split('.').pop()?.toLowerCase()}`;
        return allowedFileTypes.includes(fileType) || allowedExtensions.includes(fileExtension);
    };

    const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
        const file = event.target.files?.[0];
        if (!file) return;

        if (!validateFile(file)) {
            setError('Invalid file type. Please upload a PNG, JPG, JPEG, PDF, or PSD file.');
            return;
        }

        setError(null);
        setUploading(true);
        setUploadProgress(0);

        const formData = new FormData();
        formData.append('file', file);
        if (workOrderItemId) {
            formData.append('workOrderItemId', workOrderItemId);
        }

        try {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/api/upload', true);

            xhr.upload.onprogress = (event) => {
                if (event.lengthComputable) {
                    const progress = (event.loaded / event.total) * 100;
                    setUploadProgress(Math.round(progress));
                }
            };

            xhr.onload = () => {
                if (xhr.status === 200) {
                    const response = JSON.parse(xhr.responseText);
                    if (response.success) {
                        const newFile = { fileUrl: response.fileUrl, description: '' };
                        setUploadedFiles(prev => [...prev, newFile]);
                        onFileUploaded(response.fileUrl, '');
                        if (fileInputRef.current) {
                            fileInputRef.current.value = '';
                        }
                    } else {
                        throw new Error(response.message || 'Upload failed');
                    }
                } else {
                    throw new Error('Upload failed');
                }
            };

            xhr.onerror = () => {
                throw new Error('Upload failed');
            };

            xhr.send(formData);
        } catch (error) {
            console.error('Error uploading file:', error);
            setError('Failed to upload file. Please try again.');
        } finally {
            setUploading(false);
        }
    };

    const handleRemoveFile = (fileUrl: string) => {
        setUploadedFiles(prev => prev.filter(file => file.fileUrl !== fileUrl));
        onFileRemoved(fileUrl);
    };

    const handleDescriptionChange = (fileUrl: string, newDescription: string) => {
        setUploadedFiles(prev =>
            prev.map(file =>
                file.fileUrl === fileUrl ? { ...file, description: newDescription } : file
            )
        );
        onDescriptionChanged(fileUrl, newDescription);
    };

    const renderPreview = (file: { fileUrl: string; description: string }) => {
        const fileExtension = file.fileUrl.split('.').pop()?.toLowerCase();

        if (['jpg', 'jpeg', 'png'].includes(fileExtension || '')) {
            return (
                <Image src={file.fileUrl} alt="File preview" width={100} height={100} objectFit="contain" />
            );
        }

        // For non-image files, show an icon or text
        return (
            <div className="p-4 bg-gray-100 rounded">
                <p className="text-lg font-semibold">{fileExtension?.toUpperCase()} File</p>
                <p className="text-sm text-gray-600">Preview not available</p>
            </div>
        );
    };

    return (
        <div>
            <input
                type="file"
                ref={fileInputRef}
                onChange={handleFileChange}
                disabled={uploading}
                accept={allowedExtensions.join(',')}
                className="file-input file-input-bordered w-full max-w-xs"
            />
            <p className="text-sm text-gray-600 mt-1">
                Allowed file types: PNG, JPG, JPEG, PDF, PSD
            </p>
            {uploading && (
                <div className="mt-2">
                    <progress className="progress w-56" value={uploadProgress} max="100"></progress>
                    <p>{uploadProgress}% Uploaded</p>
                </div>
            )}
            {error && <p className="text-red-500 mt-2">{error}</p>}
            {uploadedFiles.map((file, index) => (
                <div key={index} className="mt-4 p-4 border rounded">
                    {renderPreview(file)}
                    <div className="mt-2">
                        <input
                            type="text"
                            value={file.description}
                            onChange={(e) => handleDescriptionChange(file.fileUrl, e.target.value)}
                            placeholder="Enter description"
                            className="input input-bordered w-full"
                        />
                    </div>
                    <button
                        onClick={() => handleRemoveFile(file.fileUrl)}
                        className="btn btn-sm btn-error mt-2"
                    >
                        Remove
                    </button>
                </div>
            ))}
        </div>
    );
};
export default FileUpload;

the fileURL stored in the database looks like this:

/uploads/1724947844252.png

Neither component can show the image? However, as is the usual case, it works fine when i’m running it locally.

2

Answers


  1. Chosen as BEST ANSWER

    I solved the issue. So, what was happening is that the images being uploaded to public/uploads didn't render using the Image component by Nextjs because they weren't part of the build process. This can be verified by running a yarn build in the project folder on the server and then hitting refresh and they suddenly appear. The solution is to use the standard img html component instead. Then it appears immediately.

    The other option would be using s3 or something similar for your image uploads and then you could use Nextjs's Image component instead.


  2. it looks like your Nginx it’s correctly settled up since the images loads successfully when accessed directly via https://print-portal.1905newmedia.com/uploads/1724528290711.png.

    Since the error occurs when using the Next <Image /> component, here may be a few things to check:

    1. Check <Image /> component URL: Make sure that the URL you pass to the <Image /> is correct. For instance:

      <Image
        src="https://print-portal.1905newmedia.com/uploads/1724528290711.png"
        alt="1724528290711"
        width={200}
        height={200}
      />
      
    2. Next image optimization: Next optimizes images and expect the origin of this images to be from allowed domains. Ensure you have settled your domain in your remotePatterns. For instance:

      // next.config.js | next.config.mjs
      
      module.exports = {
        images: {
          remotePatterns: [
            {
              protocol: "https",
              hostname: "print-portal.1905newmedia.com",
              port: "",
              pathname: "/uploads/**"
            }
          ]
        }
      }
      

      Feel free to check the Next.js remotePatterns documentation

    3. Check for console logs: Look at the browser console log for any errors related to images. This may be helpful highlighting the error’s origin.

    4. Debug with static HTML images: To isolate the issue you might test if the images loads with the regular HTML <img /> tag. For instance:

      <img
        src="https://print-portal.1905newmedia.com/uploads/1724528290711.png"
        alt="1724528290711"
        width={200}
        height={200}
      />
      

      If the image loads successfully, then it may be a Next <Image /> component configuration or Next issue.

    Be aware that the Next <Image /> component is NOT free. You’ll be charged if you pass certain limit. You can learn more about Next.js Image component pricing right here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search