question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

unstable_parseMultipartFormData nulls all non-file formdata fields

See original GitHub issue

What version of Remix are you using?

1.5.1

Steps to Reproduce

Installation

npx create-remix@latest ? Where would you like to create your app? ./remix-upload-test1.5.1 ? What type of app do you want to create? Just the basics ? Where do you want to deploy? Choose Remix if you’re unsure; it’s easy to change deployment targets. Remix App Server ? Do you want me to run npm install? (Y/n) y

Changes

change routes/index.ts to:

import type { ActionFunction } from '@remix-run/node'
import {
  redirect,
  unstable_createFileUploadHandler,
  unstable_parseMultipartFormData,
} from '@remix-run/node'
import { Form } from '@remix-run/react'

export const action: ActionFunction = async ({ request }) => {
  const uploadHandler = unstable_createFileUploadHandler({
    directory: './public/uploads',
    file: ({ filename }) => filename,
  })

  const formData = await unstable_parseMultipartFormData(request, uploadHandler)

  const title = formData.get('title')
  const fileId = formData.get('file')
  console.log('title', title, 'file: ', fileId)
  return redirect(``)
}

export default function Index() {
  return (
    <div>
      <h1>Upload Test</h1>
      <Form method='post' encType='multipart/form-data'>
        <label htmlFor='title'>Title</label>
        <input type='text' name='title' id='title' />

        <label htmlFor='file'>File</label>
        <input type='file' id='file' name='file' accept='application/pdf' />

        <button type='submit'>Submit</button>
      </Form>
    </div>
  )
}

Expected Behavior

I was expecting to get the title with formData.get(‘title’) as i had sent it in the post request. but i got:

title:  null file:  NodeOnDiskFile [File] {
  lastModified: 0,
  webkitRelativePath: '',
  filepath: 'public/uploads/example.pdf',
  type: 'application/pdf',
  slicer: undefined,
  name: 'example.pdf'
}

Actual Behavior

Post The post request goes through with 200:

-----------------------------
Content-Disposition: form-data; name="title"

Test title
-----------------------------
Content-Disposition: form-data; name="file"; filename="example.pdf"
Content-Type:application/pdf

However, the console.log on title gives me null

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:3
  • Comments:5

github_iconTop GitHub Comments

2reactions
machourcommented, Jun 8, 2022

Can you test the answer provided here? https://github.com/remix-run/remix/issues/3238#issuecomment-1135147298

If it works, then we need to properly document this change

0reactions
akommcommented, Nov 29, 2022

The problem is that the filter passed to the handler function is going through literally all parts, this includes regular string fields. This is a changed behavior that broke my previously functioning handler. While its not entirely wrong that all parts are treatet equally its very inconvenient because creating a uploadHandler for files, you want to handle files and pass through the parts representing simple fields.

Working currently with this one:

import {UploadHandler, UploadHandlerPart} from "@remix-run/node"

// https://github.com/remix-run/remix/issues/3409
type FilePart = Omit<UploadHandlerPart, "filename"> & {filename: string}

type MemoryUploadHandlerOptions = {
  maxPartSize: number
  accept: (part: FilePart) => boolean
}

export function createMemoryUploadHandler(
  opts: MemoryUploadHandlerOptions
): UploadHandler {
  const accept = opts.accept || (() => true)
  const maxPartSize = Math.max(1, opts.maxPartSize || 3_000_000)
  const textDecoder = new TextDecoder()

  async function getChunks(data: UploadHandlerPart["data"]) {
    const chunks: Uint8Array[] = []

    let partSize = 0
    for await (const chunk of data) {
      partSize += chunk.length

      if (maxPartSize < partSize) {
        return {chunks: null, partSize: 0}
      }

      chunks.push(chunk)
    }

    return {chunks, partSize}
  }

  async function handleFile(part: FilePart) {
    if (!accept(part)) {
      return null
    }

    const {chunks} = await getChunks(part.data)

    if (chunks == null) {
      return null
    }

    return new File(chunks, part.filename, {type: part.contentType})
  }

  async function handleString(part: UploadHandlerPart) {
    const {chunks, partSize} = await getChunks(part.data)

    if (!chunks) {
      return null
    }

    if (partSize === 0) {
      return ""
    }

    const data = new Uint8Array(partSize)

    let pointer = 0
    for (const chunk of chunks) {
      data.set(chunk, pointer)
      pointer += chunk.length
    }

    return textDecoder.decode(data)
  }

  return async part => {
    if (part.filename) {
      return handleFile({...part, filename: part.filename})
    }

    return handleString(part)
  }
}

Usage examples:

// 1.
const image = createMemoryUploadHandler({
  maxPartSize: 2_000_000,
  accept: ({contentType, filename}) => {
    if (contentType) {
      return ["image/jpeg", "image/png", "image/gif"].includes(
        contentType.toLowerCase()
      )
    }

    const ext = path.extname(filename).toLowerCase()
    return [".png", ".jpg", ".jpeg", ".gif"].includes(ext)
  }
})

// 2.
const zip = createMemoryUploadHandler({
  maxPartSize: 10_000_000,
  accept: ({contentType, filename}) => {
    if (contentType) {
      return ["application/zip", "application/x-zip-compressed"].includes(
        contentType
      )
    }
    const ext = path.extname(filename).toLowerCase()
    return ext === ".zip"
  }
})

Then use it with unstable_parseMultipartFormData as handler. This is not library level implementation, adjust as needed. You might want different maxPartSize for files and fields, etc.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is there any way to access non-file fields in a multipart/form ...
I'm trying to access some fields that have been included with the request before I parse the form (so that I can perform...
Read more >
Using FormData Objects - Web APIs | MDN
This example builds a FormData instance containing values for fields named "username", "accountnum", "userfile" and "webmasterfile", then uses ...
Read more >
Solved: Formdata is of type null - Power Platform Community
Solved: Hello, I have a html form im submiting to a Flow in Power Automate, the issue is that even though there is...
Read more >
FormData - The Modern JavaScript Tutorial
It's encoded and sent out with Content-Type: multipart/form-data . ... The difference is that .set removes all fields with the given name ...
Read more >
Validating form data - Miami University
Data validation is important to the proper functioning of any application. ... all, Causes the specified field to fail if no value is...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found