I'm trying to crop an image in a Next.js app, send it to an API route in the app and finally onto an API endpoint outside of the app. If I bypass the API route, it works OK, but not when going via it. The image data is no longer correct and can't be processed.
Client (Next.js) --> API route (Next.js) --> API Endpoint (External)
Client (Next.js) - fetch
using FormData
via POST
async function handleSave(image: Blob) {
const file = new File([image], 'avatar.png', { type: 'image/png' })
const data = new FormData()
data.append('imageFile', file)
const response = await fetch(`/api/users/${userId}/media/avatar`,
{
body: data,
method: 'POST',
}
)
// const response = await fetch (`https://localhost:1337/user/${userId}/media/avatar`, {
// method: 'POST',
// headers: {
// "Authorization": `Bearer <JWT token>`,
// },
// body: data
// })
if (response.ok) {
// handle
}
}
The commented out fetch is where I tested directly calling the external API Endpoint, this works OK.
API Route (Next.js) - take the request from the client side and forward it onto the external API endpoint.
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await cors(req, res)
const { userId } = req.query;
const { accessToken } = await getToken({ req, secret });
const response = await fetch(`${process.env.API_HOST}/user/${userId}/media/avatar`, {
method: 'POST',
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": req.headers["content-type"]
},
body: req.body
})
try {
const json = await response.json();
console.log(json)
}
finally { }
res.end()
}
API Endpoint (External)
multipart/form-data
imageFile
and is mapped to IFormFile
Once the request is passed through the API route and sent on to the external API, the image stream is no longer valid. I can see the IFormFile
object has picked up the imageFile
OK and got the relevant data.
When I bypass the Next.js API route, the upload works fine and I have noted that the IFormFile
object length is much smaller.
Going via the Next.js API route is important because it handles passing the access token to the external API and it's not intended to expose that API.
I've taken a look at Create upload files api in next.js , but I'm not sure how/if formidable
fits for this scenario?
After a lot of digging and going through many different methods, I finally found one that works.
multipart/form-data
file.formidable
to read the file, which saves to disk first, then fs
to read that file and finally used that in the FormData
.import Cors from 'cors'
import initMiddleware from '../../../../../lib/initMiddleware'
import { NextApiRequest, NextApiResponse } from 'next'
import { getToken } from 'next-auth/jwt'
import fetch from "node-fetch";
import FormData from 'form-data'
import { IncomingForm } from 'formidable'
import { promises as fs } from 'fs';
export const config = {
api: {
bodyParser: false,
},
}
const cors = initMiddleware(
Cors({
methods: ['POST']
})
)
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await cors(req, res)
const { userId } = req.query;
const { accessToken } = await getToken({ req, secret });
const fData = await new Promise<{ fields: any, files: any }>((resolve, reject) => {
const form = new IncomingForm({
multiples: false
})
form.parse(req, (err, fields, files) => {
if (err) return reject(err)
resolve({ fields, files })
})
});
const imageFile = fData.files.imageFile
const tempImagePath = imageFile?.filepath
try {
const image = await fs.readFile(tempImagePath)
const data = new FormData()
data.append('imageFile', image, { filename: 'avatar.png' })
const response = await fetch(`${process.env.API_HOST}/user/${userId}/media/avatar`, {
method: 'POST',
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": `multipart/form-data; boundary=${data.getBoundary()}`
},
body: data
})
if (!response.ok) {
}
res.status(response.status);
}
catch (error) {
}
finally {
if (tempImagePath) {
await fs.rm(tempImagePath)
}
}
res.end()
}
I had the same problem. I use nextjs api as proxy to another api server, in my case I just passed the entire request instead of the body, specifying to nextjs to not parser the request. The only thing I had to include were the headers of the original request.
import Repository, { apiUrl } from "~/repositories/Repository";
import { parse } from 'cookie';
export const config = {
api: {
bodyParser: false
}
}
export default async (req, res) => {
const { headers: { cookie, ...rest }, method } = req;
const { auth } = parse(cookie);
if (method === 'PUT') {
const headers = {
Authorization: `Bearer ${auth}`,
'content-type': rest['content-type'],
'content-length': rest['content-length']
}
return Repository.put(`${apiUrl}/user/me/files`, req, { headers: headers })
.then(response => {
const { data } = response;
return res.status(200).json(data);
})
.catch(error => {
const { response: { status, data } } = error;
return res.status(status).json(data);
})
}
else
return res.status(405);
}
I had the same issue and finally I found pretty elegant way to resolve it.
Solution source: https://github.com/vercel/next.js/discussions/15727#discussioncomment-1094126
Just in case, I'm copying the code from the source:
File upload component
const uploadFile = (file : File) => {
let url = "http://localhost:3000/api/upload";
let formData = new FormData();
formData.set("testFile", file)
fetch(url, {
method: "POST",
body: formData,
}).then(r => {
console.log(r);
})
}
/api/upload.ts:
import {NextApiRequest, NextApiResponse} from "next";
import httpProxyMiddleware from "next-http-proxy-middleware";
// For preventing header corruption, specifically Content-Length header
export const config = {
api: {
bodyParser: false,
},
}
export default (req: NextApiRequest, res: NextApiResponse) => {
httpProxyMiddleware(req, res, {
target: 'http://localhost:21942'
})
}
import formidable from "formidable";
import fs from "fs";
export const config = {
api: {
bodyParser: false
}
};
interface IFileStream {
filepath: string;
originalFilename: string;
}
const ProcessFiles = (Files: any): IFileStream[] => {
const data: IFileStream[] = [];
let index = 0;
while (Boolean(Files[`file${index}`])) {
data.push(Files[`file${index}`] as IFileStream)
index++;
}
return data;
}
export default async function handler(req: any, res: any) {
if (req.method === "POST") {
const form = new formidable.IncomingForm();
form.parse(req, async function (err, fields, files) {
const filesArray = ProcessFiles(files)
if (filesArray.length === 0) return
res.sendStatus(401).json({massages: "No File Found"});
// create 'uploads' folder if not exist
fs.mkdirSync("./public/uploads", {recursive: true});
for (let file of filesArray) {
const data = fs.readFileSync(file.filepath);
fs.writeFileSync(`./public/uploads/${Date.now().toString() +
file.originalFilename}`, data);
fs.unlinkSync(file.filepath);
}
});
return res.status(200).json({ massage: "Success" });
}
};
import axios from 'axios';
import { useState } from 'react';
const api = axios.create();
const uploadFile = async (files: File[]) => {
let formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.set(`file${i}`, files[i]);
}
await api.postForm("http://localhost:3000/api/upload", formData)
.then(res => { console.log(res) }).catch((err) => {
console.log(err) }) }
export default function Home() {
const [files, setFiles] = useState<File[] | null>([]);
const HandelUpload = async () => {
if (files?.length === 0 || files === null) return alert("No
file selected");
await uploadFile(files)
}
return (
<div>
<input type='file' multiple onChange={(event) =>
setFiles((event?.target?.files || null) as File[] | null)} />
<button onClick={HandelUpload}>Upload</button>
</div>
)
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.