Request

class
Superclass
Object
Extended With
Loggability

The Mongrel2 Request base class. Derivatives of this class represent a request from a Mongrel2 server.

Attributes

request_types[R]
body[R]

The request body data, if there is any, as an IO(ish) object

conn_id[R]

The listener ID on the server

header[R]

The Mongrel2::Table object that contains the request headers

headers[R]

The Mongrel2::Table object that contains the request headers

path[R]

The path component of the requested URL in HTTP, or the equivalent for other request types

raw[R]

The raw request content, if the request was parsed from mongrel2

sender_id[R]

The UUID of the requesting mongrel server

Public Class Methods

anchor
new( sender_id, conn_id, path, headers, body='', raw=nil )

Create a new Request object with the given sender_id, conn_id, path, headers, and body. The optional nil is for the raw request content, which can be useful later for debugging.

# File lib/mongrel2/request.rb, line 113
def initialize( sender_id, conn_id, path, headers, body='', raw=nil )

        @sender_id = sender_id
        @conn_id   = Integer( conn_id )
        @path      = path
        @headers   = Mongrel2::Table.new( headers )
        @raw       = raw

        @body      = self.make_entity_body( body )
        @response  = nil
end
anchor
parse( raw_request )

Parse the given raw_request from a Mongrel2 server and return an appropriate request object.

# File lib/mongrel2/request.rb, line 28
def self::parse( raw_request )
        sender, conn_id, path, rest = raw_request.split( ' ', 4 )
        self.log.debug "Parsing request for %p from %s:%s (rest: %p...)" %
                [ path, sender, conn_id, rest[0,20] ]

        # Extract the headers and the body, ignore the rest
        headers, rest = TNetstring.parse( rest )
        body, _       = TNetstring.parse( rest )

        # Headers will be a JSON String when not using the TNetString protocol
        if headers.is_a?( String )
                self.log.debug "  parsing old-style headers"
                headers = Yajl::Parser.parse( headers )
        end

        # This isn't supposed to happen, but guard against it anyway
        headers['METHOD'] =~ /^(\w+)$/ or
                raise Mongrel2::UnhandledMethodError, headers['METHOD']
        req_method = $1.untaint.to_sym
        self.log.debug "Request method is: %p" % [ req_method ]
        concrete_class = self.subclass_for_method( req_method )

        return concrete_class.new( sender, conn_id, path, headers, body, raw_request )
end
anchor
register_request_type( subclass, *req_methods )

Register the specified subclass as the class to instantiate when the METHOD header is one of the specified req_methods. This method exists for frameworks which wish to provide their own Request types.

For example, if your framework has a JSONRequest class that inherits from Mongrel2::JSONRequest, and you want it to be returned from Mongrel2::Request.parse for METHOD=JSON requests:

class MyFramework::JSONRequest < Mongrel2::JSONRequest
    register_request_type self, 'JSON'

    # Override #initialize to do any stuff specific to your
    # request type, but you'll likely want to super() to
    # Mongrel2::JSONRequest.
    def initialize( * )
        super
        # Do some other stuff
    end

end # class MyFramework::JSONRequest

If you wish one of your subclasses to be used instead of Mongrel2::Request for the default request class, register it with a METHOD of :__default.

# File lib/mongrel2/request.rb, line 77
def self::register_request_type( subclass, *req_methods )
        self.log.debug "Registering %p for %p requests" % [ subclass, req_methods ]
        req_methods.each do |methname|
                if methname == :__default
                        # Clear cached lookups
                        self.log.info "Registering %p as the default request type." % [ subclass ]
                        Mongrel2::Request.request_types.delete_if {|_, klass| klass == Mongrel2::Request }
                        Mongrel2::Request.request_types.default_proc = lambda {|h,k| h[k] = subclass }
                else
                        self.log.info "Registering %p for the %p method." % [ subclass, methname ]
                        Mongrel2::Request.request_types[ methname.to_sym ] = subclass
                end
        end
end
anchor
response_class()

Return the Mongrel2::Response class that corresponds with the receiver.

# File lib/mongrel2/request.rb, line 100
def self::response_class
        return Mongrel2::Response
end
anchor
subclass_for_method( methname )

Return the Mongrel2::Request class registered for the request method methname.

# File lib/mongrel2/request.rb, line 94
def self::subclass_for_method( methname )
        return Mongrel2::Request.request_types[ methname.to_sym ]
end

Public Instance Methods

anchor
body=( newbody )

Set the request's entity body to newbody. If newbody is a String-ish object (i.e., it responds to to_str), it will be wrapped in a StringIO in 'r+' mode).

# File lib/mongrel2/request.rb, line 153
def body=( newbody )
        newbody = StringIO.new( newbody, 'a+' ) if newbody.respond_to?( :to_str )
        @body = newbody
end
anchor
is_disconnect?()

Return true if the request is a special 'disconnect' notification from Mongrel2.

# File lib/mongrel2/request.rb, line 169
def is_disconnect?
        return false
end
anchor
remote_ip()

Fetch the original requestor IP address.

# File lib/mongrel2/request.rb, line 175
def remote_ip
        ips = [ self.headers.x_forwarded_for ]
        return IPAddr.new( ips.flatten.first )
end
anchor
response()

Create a Mongrel2::Response that will respond to the same server/connection as the receiver. If you wish your specialized Request class to have a corresponding response type, you can override the Mongrel2::Request.response_class method to achieve that.

# File lib/mongrel2/request.rb, line 163
def response
        return @response ||= self.class.response_class.from_request( self )
end

Async Upload Support

↑ top

Public Instance Methods

anchor
extended_reply?()

Indicate that a request is never an extended reply.

# File lib/mongrel2/request.rb, line 251
def extended_reply?
        return false
end
anchor
server_chroot()

Return the chroot directory of the mongrel2 daemon that received this request as a Pathname.

# File lib/mongrel2/request.rb, line 209
def server_chroot
        route = Mongrel2::Config::Route.for_request( self ) or
                raise Mongrel2::UploadError, "couldn't find the route config for %s" % [ self ]
        server = route.host.server

        path = server.chroot
        path = '/' if path.empty?

        return Pathname( path )
end
anchor
upload_done?()

Returns true if this request is an 'asynchronous upload done' notification.

# File lib/mongrel2/request.rb, line 229
def upload_done?
        return self.headers.member?( :x_mongrel2_upload_start ) &&
               self.headers.member?( :x_mongrel2_upload_done )
end
anchor
upload_headers_match?()

Returns true if this request is an 'asynchronous upload done' notification and the two headers match (trivial guard against forgery)

# File lib/mongrel2/request.rb, line 237
def upload_headers_match?
        return self.upload_done? &&
               self.headers.x_mongrel2_upload_start == self.headers.x_mongrel2_upload_done
end
anchor
upload_started?()

Returns true if this request is an 'asynchronous upload started' notification.

# File lib/mongrel2/request.rb, line 222
def upload_started?
        return self.headers.member?( :x_mongrel2_upload_start ) &&
               !self.headers.member?( :x_mongrel2_upload_done )
end
anchor
uploaded_file()

The Pathname, relative to Mongrel2's chroot path, of the uploaded entity body.

# File lib/mongrel2/request.rb, line 187
def uploaded_file
        raise Mongrel2::UploadError, "invalid upload: upload headers don't match" unless
                self.upload_headers_match?

        relpath = Pathname( self.headers.x_mongrel2_upload_done )
        chrooted = self.server_chroot + relpath

        if chrooted.exist?
                return chrooted
        elsif relpath.exist?
                return relpath
        else
                self.log.error "uploaded body %s not found: tried relative to cwd and server chroot (%s)" %
                        [ relpath, chrooted ]
                raise Mongrel2::UploadError,
                        "couldn't find the path to uploaded body %p." % [ chrooted.to_s ]
        end
end
anchor
valid_upload?()

Returns true if this request is an asynchronous upload, and the filename of the finished request matches the one from the starting notification.

# File lib/mongrel2/request.rb, line 245
def valid_upload?
        return self.upload_done? && self.upload_headers_match?
end

Introspection Methods

↑ top

Public Instance Methods

anchor
inspect()

Returns a string containing a human-readable representation of the Request, suitable for debugging.

# File lib/mongrel2/request.rb, line 262
def inspect
        return "#<%p:0x%016x %s (%s/%d)>" % [
                self.class,
                self.object_id * 2,
                self.inspect_details,
                self.sender_id,
                self.conn_id
        ]
end

Protected Instance Methods

anchor
inspect_details()

Return the details to include in the contents of the inspected object. This method allows other request types to provide their own details while keeping the form somewhat consistent.

# File lib/mongrel2/request.rb, line 310
def inspect_details
        return "%s -- %d headers, %p body" % [
                self.path,
                self.headers.length,
                self.body.class,
        ]
end
anchor
make_entity_body( body )

Convert the entity body into an IOish object, wrapping it in a StringIO if it doesn't already respond to :read, :pos, and :seek. If the request has valid 'X-Mongrel2-Upload-*' headers (the async upload API), a File object opened to the spool file will be returned instead.

# File lib/mongrel2/request.rb, line 281
def make_entity_body( body )
        # :TODO: Handle Content-Encoding, too.

        enc = self.headers.content_type[ /\bcharset=(\S+)/, 1 ] if self.headers.content_type

        if self.valid_upload?
                enc ||= Encoding::ASCII_8BIT
                spoolfile = self.uploaded_file
                self.log.info "Using async %s spool file %s as request entity body." % [ enc, spoolfile ]
                return spoolfile.open( 'r', encoding: enc )

        elsif !( body.respond_to?(:read) && body.respond_to?(:pos) && body.respond_to?(:seek) )
                self.log.info "Wrapping non-IO (%p) body in a StringIO" % [ body.class ] unless
                        body.is_a?( String )

                # Get the object as a String, set the encoding
                str = body.to_s
                str.force_encoding( enc ) if enc && str.encoding == Encoding::ASCII_8BIT

                return StringIO.new( str, 'r+' )
        else
                return body
        end
end