kemalcr / kemal

Fast, Effective, Simple Web Framework

Home Page:https://kemalcr.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Failed to parse multipart message: EOF reading delimiter

codenoid opened this issue · comments

Description

Exception: Failed to parse multipart message: EOF reading delimiter (HTTP::Multipart::Error)

Steps to Reproduce

  1. puts env.redirect with HTTP::FormData.parse
require "kemal"

post "/" do |env|
  HTTP::FormData.parse(env.request) do |upload|
    filename = upload.filename

    if !filename.is_a?(String)
      "No filename included in upload"
    else
      file_path = ::File.join [Kemal.config.public_folder, "uploads/", filename]
      File.open(file_path, "w") do |f|
        IO.copy(upload.body, f)
      end
    end
  end

  env.redirect "/"
end

Kemal.run

image

Actual behavior: [What actually happens]

2018-09-09 02:25:24 +07:00 302 POST / 8.56ms                     
Exception: Failed to parse multipart message: EOF reading delimiter (HTTP::Multipart::Error)                
  from /usr/share/crystal/src/http/multipart/parser.cr:122:7 in 'fail'                                      
  from /usr/share/crystal/src/http/multipart/parser.cr:0:7 in 'close_delimiter?'                            
  from src/routes/content-management.cr:69:33 in '->'                                                       
  from lib/kemal/src/kemal/route.cr:255:3 in '->'                                                           
  from lib/kemal/src/kemal/route_handler.cr:255:3 in 'process_request'                                      
  from lib/kemal/src/kemal/route_handler.cr:15:7 in 'call'                                                  
  from /usr/share/crystal/src/http/server/handler.cr:24:7 in 'call_next'                                    
  from lib/kemal/src/kemal/websocket_handler.cr:13:14 in 'call'                                             
  from /usr/share/crystal/src/http/server/handler.cr:24:7 in 'call_next'                                    
  from lib/kemal/src/kemal/static_file_handler.cr:12:11 in 'call'                                           
  from /usr/share/crystal/src/http/server/handler.cr:24:7 in 'call_next'                                    
  from lib/kemal/src/kemal/exception_handler.cr:8:7 in 'call'                                               
  from /usr/share/crystal/src/http/server/handler.cr:24:7 in 'call_next'                                    
  from lib/kemal/src/kemal/log_handler.cr:10:35 in 'call'                                                   
  from /usr/share/crystal/src/http/server/handler.cr:24:7 in 'call_next'                                    
  from lib/kemal/src/kemal/init_handler.cr:12:7 in 'call'                                                   
  from /usr/share/crystal/src/http/server/request_processor.cr:39:11 in 'process'                           
  from /usr/share/crystal/src/http/server/request_processor.cr:16:3 in 'process'                            
  from /usr/share/crystal/src/http/server.cr:402:5 in 'handle_client'                                       
  from /usr/share/crystal/src/http/server.cr:368:13 in '->'                                                 
  from /usr/share/crystal/src/fiber.cr:255:3 in 'run'                                                       
  from /usr/share/crystal/src/fiber.cr:29:34 in '->'                                                        
  from ??? 

Versions

Ubuntu 16.04, Crystal 0.26.1, Kemal 0.24.0

To reproduce this, you need to provide the HTTP request sent to the server.

I get that particular message when my form submission is genuinely messed up. What we need to know is what data is being sent by your client to see if it looks good.

Dunno if it's the same problem but maybe also:

require "kemal"

post "/upload" do |env|
  HTTP::FormData.parse(env.request) do |upload|
  end
end

Kemal.run

with this client: curl -s http://127.0.0.1:3000/upload -X POST --data 'x=1'

results in Exception: Cannot extract form-data from HTTP request: could not find boundary in Content-Type (HTTP::FormData::Error)

and with curl -s http://127.0.0.1:3000/upload -X POST get

Exception: Cannot extract form-data from HTTP request: body is empty (HTTP::FormData::Error)

@rdp Your curl requests don't send multipart/form-data content. So obviously, HTTP::FormData can't parse it and the error messages are very explicit about that.

@codenoid The exact HTTP request is not reproducible from your screenshot (it doesn't even say which program UI it shows). Could you please provide some code that I can run to send the same request? Preferably a curl command or Crystal source code.

I wouldn't expect it to crash unless there's some way to first check if there is formdata or not perhaps? Anyway I suppose it's part of the ongoing conversation for #465 for now...

Well, you're using HTTP::FormData on you own account. So you need to ensure it's safe. A minimal precaution would be to check if the request's content type is actually multipart/form-data. This would already detect your example requests.

Apart from that, it's obviously always possible that the message format is compromised, so to be safe you can wrap the parsing in a begin / rescue.

As @straight-shoota already explained, Kemal does not make any assumptions about the message format. It's the developers' responsibility to actually make it safe.

I believe this may be related to a bug I opened a PR for: #567

For followers, this error message meant the following, I was parsing params twice:

post "/some_path" do |env|
  some_parameter = env.params.body["my_form_parameter_name"] # get a POST param
  HTTP::FormData.parse(env.request) do |part| # parse file data
    ...
  end

It seems that using params.body as well as FormData.parse causes it to "parse twice" and blow up. I'm guessing that params.body discards file input (?)

Workaround is to only use FormData.parse like

post "/some_path" do |env|
  HTTP::FormData.parse(env.request) do |part|
     if part.name == "file_upload"
       file_contents = part.body.gets_to_end
       filename = part.headers.filename
    elsif part.name == "my_form_parameter_name"
       some_parameter = part.body.gets_to_end
    end
  end
end

Which works.

As a side note it'd be really nice if the FormData crystal docs described file uploads better, but unrelated. Thanks!

Yeah, HTTP::Server::Context#params is a Kemal extension which parses parameters from the URL and body, which results in consuming the body. This might not be evident from the looks of it.
That's one reason why this is not part of the stdlib implementation.