Uploading files using Redwood Function and Fastify

Hello!

I’ve encountered an issue with uploading files using a Redwood Function… Currently, I’m calling a Redwood-generated function using an axios “post” request that sends an instance of FormData that contains a file, and includes this header: 'Content-Type': 'multipart/form-data'

This causes the following error:

{
	"statusCode": 400,
	"code": "FST_ERR_CTP_INVALID_CONTENT_LENGTH",
	"error": "Bad Request",
	"message": "Request body size did not match Content-Length"
}

…some research indicates this is how Fastify works under the hood, and can be dealt with by using an appropriate Fastify plugin to parse the 'multipart/form-data' content type.

The Redwood docs even mention this here, but, the server.config.js file I see seems to differ quite a bit from the example in the docs (I’m on Redwood 7.0.2).

Does anyone know how to fix this, either via an appropriate Fastify plugin, or perhaps via a different approach?

Thanks!

Hey @nullsett! Those docs you linked to reference the server file which is the successor to api/server.config.js. I’ll try the example again in the meantime to ensure it works, but were you referring to a different api/server.config.js file you’re seeing somewhere?

Hey @dom thanks for the reply!

The file I see is at api/server.config.js… it contains a fastify config object (comment preceding the line is: @type {import('fastify').FastifyServerOptions})… as well as a function called configureFastify().

I most recently was following the docs here (I know it’s older version of Redwood but it looked like it might head somewhere for me): App Configuration | RedwoodJS Docs

…I was trying to use the fastify multipart plugin here like so:

await fastify.register(import('@fastify/multipart'))

…but hadn’t gotten anything working yet.

Anyhow, thanks for having a peek, curious to hear what you think might be the best next steps!

Hmmm I’m having trouble reproducing the error; now that I’m looking at the code again, I’m remembering that Redwood already configures the fastify server to accept multipart/form-data:

So you shouldn’t need to re-register that or anything. All those docs are talking about that you linked to is how to make it accept other MIME types. Are you posting an image or PDF? And are you posting it via curl or postman or programatically?

If you’re on Redwood v7.0.2 and want to configure the server, I’d recommend not using the api/server.config.js file anymore (you can delete it) and instead setting up the server file (similar names—sorry!) via yarn rw setup server-file. That will give you a first-class file at api/src/server.ts where you can do all the same things and more in a more straightforward way.

Ah gotcha, word I’ll delete the server.config.js file!

Here’s where I’m uploading an image from the web side:

const config = {
  headers: {
    'Content-Type': 'multipart/form-data',
  },
}

const formData = new FormData()
formData.append('file', merchImage)
formData.append('siteId', user.siteId)

await axios.post(`${RWJS_API_URL}/${functionName}`, formData, config)

…and this is hitting a function generated with yarn rw generate function funcName.

merchImage is unsurprisingly just referencing the result of a user selecting a file using a file input (e.g. event.target.files[0]).

Thanks again for the help!

Do you know if axios automatically includes the other headers you need in your request based on the MIME type like Content-Length?

It looks like Content-Length gets set…

…here’s the request headers on a recent try I did, according to Firefox’s inspector:

{
	"Request Headers (854 B)": {
		"headers": [
			{
				"name": "Accept",
				"value": "application/json, text/plain, */*"
			},
			{
				"name": "Accept-Encoding",
				"value": "gzip, deflate, br"
			},
			{
				"name": "Accept-Language",
				"value": "en-US,en;q=0.5"
			},
			{
				"name": "Connection",
				"value": "keep-alive"
			},
			{
				"name": "Content-Length",
				"value": "181340"
			},
			{
				"name": "Content-Type",
				"value": "multipart/form-data; boundary=---------------------------108164333610057212582968650341"
			},
			{
				"name": "Cookie",
				"value": "_ga=GA1.1.1148230033.1691728415; csrftoken=IEJLFHUbLUth8IpHtEtJ4f2o1KrhfFUA; _ga_6CFS9WMQEY=GS1.1.1707607606.22.0.1707607606.0.0.0; _legacy_auth0.74KAkb6pe0HR1pVytzMjHr1RmjPyfn2z.is.authenticated=true; auth0.74KAkb6pe0HR1pVytzMjHr1RmjPyfn2z.is.authenticated=true"
			},
			{
				"name": "DNT",
				"value": "1"
			},
			{
				"name": "Host",
				"value": "localhost:8910"
			},
			{
				"name": "Origin",
				"value": "http://localhost:8910"
			},
			{
				"name": "Referer",
				"value": "http://localhost:8910/merch"
			},
			{
				"name": "Sec-Fetch-Dest",
				"value": "empty"
			},
			{
				"name": "Sec-Fetch-Mode",
				"value": "cors"
			},
			{
				"name": "Sec-Fetch-Site",
				"value": "same-origin"
			},
			{
				"name": "User-Agent",
				"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0"
			}
		]
	}
}

@nullsett Did you manage to fix the issue at the end?
I am trying to create a custom function to upload files but I’m having the same issue.
I’m calling my custom function with:

const formData = new FormData()
formData.append('file', blob)

await fetch('/.redwood/functions/uploadDocs', {
  method: 'POST',
  body: formData,
})

and getting the following response:

{
    "statusCode": 400,
    "code": "FST_ERR_CTP_INVALID_CONTENT_LENGTH",
    "error": "Bad Request",
    "message": "Request body size did not match Content-Length"
}

There seems to be something wrong on how the request body size is calculated on the server since the Content-Length header is automatically calculated by the browser.

I tried to dig into this a bit and made some initial discoveries but could not solve the issue entirely though.
What I discovered is that if I comment out this code in the api-server package redwood/packages/api-server/src/plugins/api.ts at 605545e81b28e7e83a5f3d04464ad46847605293 · redwoodjs/redwood · GitHub and change it with:

fastify.addContentTypeParser(
  ['application/x-www-form-urlencoded', 'multipart/form-data'],
  function (req, payload, done) {
    done(null, req)
  }
)

the fetch will start working correctly but I couldn’t access the request body int he custom function probably because the content parser is not configured properly.

Another interesting thing is that if instead of changing the code in the api-server package I only comment it out and then add

fastify.addContentTypeParser(
  ['application/x-www-form-urlencoded', 'multipart/form-data'],
  function (req, payload, done) {
    done(null, req)
  }
)

to my api/src/server.ts the fetch will still not work.
Might be something to look into, it would be strange if the addContentTypeParser added in api/src/server.ts would not be applied.

Hey there @c-ciobanu ! Incidentally, I never solved this… I temporary reverted back to my solution of uploading the file directly to AWS from the front-end, just decided I’d spent enough time chasing the issue and had to move on.

I’d love to get a solution though, and what you’ve mentioned is interesting… I honestly may not get to mess with it more for a few weeks though.