When I make a request to my backend using Postman everything works just fine, but when I try to make the same request in my client-side code req.file is not populated, and neither is req.body.
My API route: (Which should be okay)
router.route('/:id')
.put(upload.single('zip'), asyncHandler(async (req, res) => {
// req.file = undefined
// I left out all the logic as how I handle the data is irrelevant,
// I just need req.file to be populated in my client side request
});
The Postman request that comes through just fine, as expected: https://share.mirasaki.dev/viBO2/QUYuhEWa94.png
The client/frontend code:
// State management
const [ file, setFile ] = useState(undefined);
const [ showErrorFeedback, setShowErrorFeedback ] = useState(false);
const [ submitState, setSubmitState ] = useState({
error: null,
message: null
});
// Save file in state on change
const handleFileChange = (e) => {
e.target.files && setFile(e.target.files[0]);
};
// Handle the form submit
const handleSubmit = async (event, url) => {
event.preventDefault();
// Return and show error if no file was uploaded
if (!file) {
setSubmitState({
error: 'No file was uploaded, this action has been cancelled'
});
setShowErrorFeedback(true);
return;
}
// Constructing the form data
const formData = new FormData();
formData.append('zip', file); // HTML5 File object from state
// PUT the form data
try {
const response = await axios.put(url, formData);
} catch (err) {
setSubmitState({ error: err.message });
setShowErrorFeedback(true);
}
};
return (
<Container>
<div><DashboardNavigation guild={props.guild}/></div>
{/* Feedback */}
{submitState.error && <Alert show={showErrorFeedback} variant="danger">
<Alert.Heading>Encountered an error</Alert.Heading>
<p>{submitState.error}</p>
<div className="d-flex justify-content-end">
<Button onClick={() => setShowErrorFeedback(false)} variant="outline-dark">
Close
</Button>
</div>
</Alert>}
{/* Zip file uploads */}
{zipUploads.map((zipCfg) => {
const prop = props[zipCfg.propId];
return (
<Form
key={zipCfg.endpoint}
className={styles.form}
// action={`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/${zipCfg.endpoint}/${router.query.id}`}
// method='put'
>
<h2>{zipCfg.name}</h2>
<p>{zipCfg.description}</p>
<Form.Group controlId={`control-${zipCfg.name}`}>
<Form.Label>{zipCfg.path}</Form.Label>
<Form.Control
required
type="file"
name='zip'
accept={zipCfg.accept}
onChange={handleFileChange}
/>
</Form.Group>
<Button
variant="primary"
type="button"
className={styles.formSubmitButton}
onClick={(event) => handleSubmit(
event,
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/${zipCfg.endpoint}/${router.query.id}`
)}
>
Submit
</Button>
{prop && <p className={styles.formFooter}>Updated: {prop.updatedAt}</p>}
</Form>
);
})}
{/* JSON OUTPUT [DEV] */}
<pre style={{ color: 'white' }}>
{JSON.stringify(props, null, 2)}
</pre>
</Container>
);
What I've tried:
I've tried so many different approaches. I've tried Axios and node:fetch to make the request. I've tried setting the Content-Type header to multipart/form-data, form-data, and application/x-www-form-urlencoded (and leaving it undefined) with every combination of Axios & node:fetch. I've tried FormData.set, FormData.append, I've tried constructing a new HTML5 File object (new File(file, 'zip', { name: 'zip' })) to try and overwrite file names dynamically, I thought maybe that was the issue instead of the form data key name
I've been banging my head at this for hours to no avail. I've searched google far and wide, but can't find any solution
Yes, I've read through the multer questions on Stack Overflow, none of which resolved my issue.
I'm at a loss here, I am completely clueless