POST request parsing error: could not find boundary in Content-Type

So, I basically just copied the code from the documentation (wrapped it in a method, but quite certain everything being passed to it is what’s needed - specifically the request object itself). The HTML form is very simple, not even a file upload field. Just a few text-fields and a few chekcboxes.

Here’s the error:

2023-03-26T11:37:36.622494Z  ERROR - http.server: Unhandled exception on HTTP::Handler
Cannot extract form-data from HTTP request: could not find boundary in Content-Type (HTTP::FormData::Error)
  from /usr/share/crystal/src/http/formdata.cr:121:5 in 'parse_post_params'

Below is the code of the HTML form:

<form action="/signup" method="POST">
    <input type="password" name="password" disabled="disabled"/><!-- auto-generated -->
    <input type="text" name="display_name" value=""/>
    <input name="cookies" type="checkbox"/>
    <input name="terms" type="checkbox"/>
</form>

And here’s one “almost” copied from the documentation code for handling POST params. The method receives context.requests object, so it should do just fine, yet the error trace only goes so far as the method call itself (not a line of the method mentioned inside).

# req variable here is context.request
def parse_post_params(req)
  # The commented parts are irrelevant, I reckoned, as the error occurs much earlier.
  if req.method != "GET"
    name = nil
    file = nil
    HTTP::FormData.parse(req) do |part|
      case part.name
      when "name"
        name = part.body.gets_to_end
      when "file"
        file = File.tempfile("upload") do |file|
          IO.copy(part.body, file)
        end
      end
    end

    unless name && file
      puts "ERROR: bad request"
      #context.response.respond_with_status(:bad_request)
      #next
    end

    #context.response << file.path
  end
end

Can anyone shed some light on it? It looks a little complex compared to parsing GET params and, while on the protocol level I can see why, I don’t see why it shall be so on the language/stdlib level.

UPDATE: the name of my method just so happened to be the same as the method from /usr/share/crystal/src/http/formdata.cr:121 I changed the name, it made no difference.

The method you copied from looks to be accepting a request made by the curl command:

curl http://localhost:8085/ -F name=foo -F file=@/path/to/test.file

That last flag file=@/path/to/test.file tells curl that it’s uploading a file and it will automatically set the Content-Type header to multipart/form-data, as well as autogenerate a boundary for said formdata.

If you’re receiving a request from a browser for that particular form, your Content-Type header is probably application/x-www-form-urlencoded, which can be safely parsed from the URI::Params class instead:

HTTP::Params.parse(req.body.gets_to_end)

Alternatively, it looks like you can set the enctype attribute within the form itself to change this behavior back to multipart/form-data. Today I learned that this documentation exists.

Hope this helps!

Ah, indeed, the request actually came from the browser, but I forgot about the multipart/form-data that shall be set. Thank you, this must resolve my issue.

1 Like

I’ve actually tried adding both enctype="multipart/form-data" or application/x-www-form-urlencoded – as you suggested (obviously, restarting and app and reloading the form page afterwards).

If I set form’senctype="multipart/form-data". then the error remains as described above. Furthermore, when I hit it with the suggested curl request, it demands some field-related header, which I’m not sure how to provide and what would be the point anyway if first and first-most this must work with a browser.

When I change form
to enctype="application/x-www-form-urlencoded", then use your suggested code
HTTP::Params.parse(req.body.gets_to_end) fails during the compilation with the following error:

 23 | return HTTP::Params.parse(req.body.gets_to_end)
                                         ^----------
Error: undefined method 'gets_to_end' for Nil (compile-time type is (IO | Nil))

The Nginx in my case is proxying the requests to the app. I checked which parameters/headers/data are being sent and even made my app display content-length (which is around 400) for the request. Inspecting the request on the browser side shows no issues, all sent as intended. It seems like it is only the request.body that either nginx or Crystal have a problem with and I doubt it’s Nginx, as I used this configuration before on other websites without a problem.

And to be clear: the objects context.request and context.request.body both exists in my program, only once it’s passed to something like HTTP::FormData.parse(), then I get the runtime error.

The compile-time error I’m not so sure why it occurs, perhaps someone could advise on that separately.

1 Like

Excellent! And good news! That compiler error is super easy to get around (and apologies for supplying a technically bad snippet of code). That compiler error is simply stating that your code could be handling a request will a nil body (I.e. A POST request that doesn’t have a body). The simple solution would be:

HTTP::Params.parse(req.body.not_nil!.gets_to_end)

This is kind of a cop out, since you didn’t actually handle the case of an nil body request. The more correct solution would be:

return unless body = req.body
HTTP::Params.parse(body.gets_to_end)

As the compiler is able to infer that body is not nil due to the earlier check.

I don’t know what’s up with the form-data type, I usually use the url encoded type instead.

(I typed this on a phone, sorry for any typos or autocorrect snafus)

1 Like

Thank you very much, this buys me time. I don’t yet need to upload files – besides, I had read on some other page of the Crystal-lang documentation website and there seems to be another way to upload files.

But for now, all I need are text-fields. The code finally works and the params are being parsed correctly. Thank you again.

1 Like