17

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)

  • ASP.Net Core Web API
  • Request should be multipart/form-data
  • Request should contain image in the 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.

enter image description here

When I bypass the Next.js API route, the upload works fine and I have noted that the IFormFile object length is much smaller.

enter image description here

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?

4 Answers 4

9

After a lot of digging and going through many different methods, I finally found one that works.

  • I didn't find an explanation as to why the image data was corrupt and meant I couldn't use the original multipart/form-data file.
  • Using formidable to read the file, which saves to disk first, then fs to read that file and finally used that in the FormData.
  • I don't know how performant this is and I find it uncomfortable having the images saved to disk first instead of just kept in memory.
  • Need to go over this again and make sure it's all secure from attack vectors, e.g. file size
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()
}
Sign up to request clarification or add additional context in comments.

3 Comments

Worth noting, Vercel has a 5mb payload limit for API calls. Ended up not using this after all!
I'm in a similiar problem: stackoverflow.com/q/72262003/1385183 . But thank you for your solution. I got it working. It would still be good to hear from somebody, why the image data gets corrupt. Is it because the content-type changes when it goes to the API route. Or is this an Next.js issue?
For me the problem was to read the file with createReadStream instead of using fs readFile promise. So thank you, Jason!
6

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'
    })
}

2 Comments

Lock of explaination
On Next.js 14 the config object is deprecated but you can get the raw data from req.text() as Rob Lee commented here
4

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);
}

2 Comments

Worked for me. The issue was req.body... it did not match the payload that was posted, the content-length received by NextJS was different from req.body.length.
Please consider removing cookie/authorization related code from this post and keep it minimal.
0

I struggled A LOT with this problem, so i want to contribute to @Jason Bert's answer because that almost worked, but i had to make another change

Instead of using pure node-fetch, i used axios, and instead of sending a buffer, i casted into a blob and to a binary file, and that worked.

const request = async (req: NextApiRequest, image: Buffer) => {
    const data = new FormData()
    const blob = new Blob([image], {type: 'application/octet-stream'})
    data.append('image', blob)

    return axios.post(`${process.env.API_URL}/api/upload-attachment`, data, {
        headers: req.headers as any,
    })
}

So this is how the final code looks pages/api/form/[[...slug]].ts

import { NextApiRequest, NextApiResponse } from 'next'
import { IncomingForm, Fields, Files } from 'formidable'
import { promises as fs } from 'fs'
import axios from 'axios'

export const config = {api: {bodyParser: false}}

const parseFormData = (req: NextApiRequest): Promise<{fields: Fields, files: Files}> => new Promise((resolve, reject) => {
    const form = new IncomingForm({multiples: false})
    form.parse(req, (err, fields, files) => {
        if (err) return reject(err)
        resolve({ fields, files })
    })
})

const request = async (req: NextApiRequest, image: Buffer) => {
    const data = new FormData()
    const blob = new Blob([image], {type: 'application/octet-stream'})
    data.append('image', blob)

    return axios.post(`${process.env.API_URL}/api/upload-attachment`, data, {
        headers: req.headers as any,
    })
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    const fData = await parseFormData(req)

    const imageFile = fData.files.image as any
    const tempImagePath = imageFile.path
    const image = await fs.readFile(tempImagePath)
    try {
        const response = await request(req, image)
        res.status(response.status).json(response.data)
    }
    catch (error: any) {
        console.error(error)
        res.status(500).json({error: error.message})
    }
    finally {
        fs.rm(tempImagePath)
    }

    res.end()
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.