The Crystal Programming Language Forum

Adding helpers to HTTP::Context to handle uploads and params

Hello everyone!

Can someone tell me if this is good enough to gather all uploads and combine params Sinatra style? Also, I can’t seem to get that size check past the compiler no matter what I do in the uploads def (I imagine it must be something really simple)! Can someone please help? Thanks!

class HTTP::Server::Context
  def uploads
    items = Hash(String, HTTP::FormData::Part).new
    HTTP::FormData.parse(request) { |part|
      # size = part.size if part && part.size.is_a?(UInt64)
      items[part.name] = part if part.size > 0
    }
    items
  end

  def params
    x = String.build { |x|
      x << request.query
      x << "&"
      if b = request.body
        x << b.gets_to_end
      end
    }

    @params ||= HTTP::Params.parse(x)
  end
end

Hi!

I’m not sure… why do you need to reopen HTTP::Server::Context? It seems like you can have the same thing by passing the context to a method defined somewhere else?

The HTTP::FormData::Part#size has UInt64?. If you want to check if it’s not nil and > 0 you need to assign it to a local variable first. The compiler can’t assume that the same method will return the same value.

This won’t compile:

part.size && part.size > 0

This will compile:

(size = part.size) && size > 0


Also, storing the HTTP::FormData::Part will not work. Those have a IO that are ment to be consumed inside the HTTP::FormData.parse. You will need to copie the part.body to an IO::Memory or something alike.

The std-lib does not encourage that becuase you will increase the memory footprint if the payload is big.

2 Likes

I’d advise caution storing multipart uploads in memory. If someone uploads a sufficiently large file, they can crash your app. It might be better to pipe it to a temp file on disk the way Rails does.

In my apps, I usually wrap the HTTP::Request object with my own Request proxy object that provides methods like form_params (for application/x-www-form-urlencoded data in POST bodies) that works exactly like HTTP::Request#query_params, but the input is the body instead of the query string.

For parsing multipart/form-data, I typically use HTTP::FormData.parse in-place because there’s usually only one place in the app that I use it and parsing it in-place lets me pipe it straight from the request to its destination, such as S3 (if I’m not uploading directly to S3 from the browser). You definitely don’t want to store HTTP::FormData::Part objects in a hash, though, because they read directly from the socket.

The HTTP request is also only intended to be parsed once, so that makes your uploads method usable only once. If you wanted to make a general-purpose uploads method, you’ll want to pipe the data into temp files and return a memoized hash that maps the param names to those temp files.

Thanks everyone for your reply! That really helps. I was just messing around trying to DRY up some code. Point taken, will look for a better way. Thanks again!