By default, Mongrel will delete the HTTP request body and short
circuit calling any handlers if a request is interrupted or
incomplete. Unfortunately, this breaks any attempt to correctly
handle a partial PUT. (BTW, PUT is *way* more efficient for uploads
compared to POST which requires a MIME parsing step.) So, about a
month ago I wrote up some patches to Mongrel 0.3.18 that allowed
Mongrel to properly handle partial PUT requests. I sent the patch &
tests to Zed for comment but never really heard back.
It occurred to me that there might be a better way to handle things,
so here's my second stab at writing a patch that I hope will be
accepted into the Mongrel trunk. This patch was written against
Mongrel 1.0. The original patch detected if the request was a PUT and
ran various special case code. This approach makes handling partial
or interrupted requests in a generic way and pushes the
responsibility for accepting or rejecting them back onto the handler.
If there's no chance this will be accepted into trunk, please let me
know. I'd be happy to provide this as a "plugin" to monkey-patch
mongrel for any people interested in this functionality.
So, the patch touches three (3) classes and makes minor modifications.
1. class HttpHandler
Adds a single method called #accept_partial_requests? which is false
by default since most handlers do NOT want to handle a partial
request. Those that do can simply override this method in the
subclass. Used in HttpServer#process_client for determining if a
handler should get called.
2. class HttpRequest
Changes the logic in the #read_body method so it no longer deletes
the TempFile and sets @body to nil as a "signal" that the request was
interrupted. Adds a #attr_reader called #interrupted? for use outside
the class along with the appropriate initialization statement (this
is the new "signal"). Lastly, adds a method called #cleanup which
essentially restores the original activity from #read_body and
deletes the TempFile.
3. class HttpServer
Some logic changes to the #process_client method which is the main
workhorse of the class. It deletes the original statement that short
circuits the loop ( break if request.body == nil) and instead pushes
that check to the loop invoking all the handlers. The handler loop
now checks each handler to verify #handler.accept_partial_requests?
before passing a partial request to the handler. If it's a complete
request, the original calling semantics apply. This means all
existing handlers would continue to function as they did before. Only
those handlers which specifically override #accept_partial_requests?
will ever get called with a partial/malformed request.
I'm a little concerned that these extra checks would be a performance
drag in the general case. Any feedback on improvements here would be
much appreciated.
Lastly, the end of the method then calls #request.cleanup to delete
any lingering TempFile.
Anyway, enough talk... time for the code. It's all listed below,
though currently *untested*. I want to get feedback from Zed and the
group before I take the extra step of adding and testing all of this
(though I expect it to pass all tests just like the original patch).
Thanks for your attention.
cr
---- code below ----
module Mongrel
class HttpHandler
# NEW
def accept_partial_requests?
false
end
end
class HttpRequest
# NEW
attr_reader :interrupted?
def initialize
# original code plus...
self.interrupted? = false
end
# modify #read_body so it does NOT delete the @body if an
exception is raised.
def read_body(remain, total)
begin
# write the odd sized chunk first
@params.http_body = read_socket(remain % Const::CHUNK_SIZE)
remain -= @body.write(@params.http_body)
update_request_progress(remain, total)
# then stream out nothing but perfectly sized chunks
until remain <= 0 or @socket.closed?
# ASSUME: we are writing to a disk and these writes always
write the requested amount
@params.http_body = read_socket(Const::CHUNK_SIZE)
remain -= @body.write(@params.http_body)
update_request_progress(remain, total)
end
rescue Object
STDERR.puts "ERROR reading http body: #$!"
$!.backtrace.join("\n")
@socket.close rescue Object
self.interrupted? = true # NEW
end
end
# NEW
# clean up any lingering temp files
def cleanup
@body.delete if @body.class == Tempfile && interrupted?
end
end
class HttpServer
def process_client(client)
begin
# deleted for brevity
while nparsed < data.length
nparsed = parser.execute(params, data, nparsed)
if parser.finished?
# deleted for brevity
if handlers
# deleted for brevity
# select handlers that want more detailed request
notification
notifiers = handlers.select { |h| h.request_notify }
request = HttpRequest.new(params, client, notifiers)
# break if request.body == nil ---- REMOVED
# request is good so far, continue processing the
response
response = HttpResponse.new(client) unless
request.interrupted? # NEW statement modifier
# Process each handler in registered order until we
run out or one finalizes the response.
if request.interrupted?
handlers.each do |handler|
handler.process(request, nil) if
handler.accept_partial_requests
end
else
# ORIGINAL logic
handlers.each do |handler|
handler.process(request, response)
break if response.done or client.closed?
end
end
# NEW
# In the case of large file uploads the user could
close the socket, so the request is in a
# partial state. Clean up after it by removing any
TempFile that may exist.
request.cleanup if request.interrupted?
# And finally, if nobody closed the response off, we
finalize it.
unless response.done or client.closed?
response.finished
end
else
# brevity!
end
break #done
else
# deleted for brevity
end
end
rescue
EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
# deleted for brevity
end
end
end
end
_______________________________________________
Mongrel-users mailing list
[email protected]
http://rubyforge.org/mailman/listinfo/mongrel-users