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
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.
it looks like your
Nginx
it’s correctly settled up since the images loads successfully when accessed directly viahttps://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:Check
<Image />
component URL: Make sure that the URL you pass to the<Image />
is correct. For instance: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:Feel free to check the Next.js
remotePatterns
documentationCheck for console logs: Look at the browser console log for any errors related to images. This may be helpful highlighting the error’s origin.
Debug with static
HTML
images: To isolate the issue you might test if the images loads with the regularHTML
<img />
tag. For instance:If the image loads successfully, then it may be a Next
<Image />
component configuration or Next issue.