1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/webrick/httpresponse.rb
normal 3b92c63158 webrick/httpresponse: minor cleanups to reduce memory use
I never knew "format" was a global method alias for "sprintf";
so it was confusing to me.  Normally, one would use "sprintf"
since it's also available in many other languages, but
Integer#to_s avoids parsing a format string so it's less
bug-prone.

Furthermore, favor string interpolation over String#<< since it
is easier for the VM to optimize memory allocation (as in r60320).
Interpolation also reduces method calls and memory overhead
for inline method cache.

Finally, ensure we clear all short-lived buffers for body
responses.  A similar change was made and measured for Net::*
in r58840 showing a large memory reduction on some workloads.

* webrick/httpresponse.rb (send_body_io): favor String#to_s,
  reduce method calls for String#<<,
  clear `buf' when done,
  avoid extra String#bytesize calls

* (send_body_string): ditto

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@60586 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2017-10-31 01:37:37 +00:00

505 lines
12 KiB
Ruby

# frozen_string_literal: false
#
# httpresponse.rb -- HTTPResponse Class
#
# Author: IPR -- Internet Programming with Ruby -- writers
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
# reserved.
#
# $IPR: httpresponse.rb,v 1.45 2003/07/11 11:02:25 gotoyuzo Exp $
require 'time'
require 'webrick/httpversion'
require 'webrick/htmlutils'
require 'webrick/httputils'
require 'webrick/httpstatus'
module WEBrick
##
# An HTTP response. This is filled in by the service or do_* methods of a
# WEBrick HTTP Servlet.
class HTTPResponse
##
# HTTP Response version
attr_reader :http_version
##
# Response status code (200)
attr_reader :status
##
# Response header
attr_reader :header
##
# Response cookies
attr_reader :cookies
##
# Response reason phrase ("OK")
attr_accessor :reason_phrase
##
# Body may be a String or IO-like object that responds to #read and
# #readpartial.
attr_accessor :body
##
# Request method for this response
attr_accessor :request_method
##
# Request URI for this response
attr_accessor :request_uri
##
# Request HTTP version for this response
attr_accessor :request_http_version
##
# Filename of the static file in this response. Only used by the
# FileHandler servlet.
attr_accessor :filename
##
# Is this a keep-alive response?
attr_accessor :keep_alive
##
# Configuration for this response
attr_reader :config
##
# Bytes sent in this response
attr_reader :sent_size
##
# Creates a new HTTP response object. WEBrick::Config::HTTP is the
# default configuration.
def initialize(config)
@config = config
@buffer_size = config[:OutputBufferSize]
@logger = config[:Logger]
@header = Hash.new
@status = HTTPStatus::RC_OK
@reason_phrase = nil
@http_version = HTTPVersion::convert(@config[:HTTPVersion])
@body = ''
@keep_alive = true
@cookies = []
@request_method = nil
@request_uri = nil
@request_http_version = @http_version # temporary
@chunked = false
@filename = nil
@sent_size = 0
end
##
# The response's HTTP status line
def status_line
"HTTP/#@http_version #@status #@reason_phrase #{CRLF}"
end
##
# Sets the response's status to the +status+ code
def status=(status)
@status = status
@reason_phrase = HTTPStatus::reason_phrase(status)
end
##
# Retrieves the response header +field+
def [](field)
@header[field.downcase]
end
##
# Sets the response header +field+ to +value+
def []=(field, value)
@header[field.downcase] = value.to_s
end
##
# The content-length header
def content_length
if len = self['content-length']
return Integer(len)
end
end
##
# Sets the content-length header to +len+
def content_length=(len)
self['content-length'] = len.to_s
end
##
# The content-type header
def content_type
self['content-type']
end
##
# Sets the content-type header to +type+
def content_type=(type)
self['content-type'] = type
end
##
# Iterates over each header in the response
def each
@header.each{|field, value| yield(field, value) }
end
##
# Will this response body be returned using chunked transfer-encoding?
def chunked?
@chunked
end
##
# Enables chunked transfer encoding.
def chunked=(val)
@chunked = val ? true : false
end
##
# Will this response's connection be kept alive?
def keep_alive?
@keep_alive
end
##
# Sends the response on +socket+
def send_response(socket) # :nodoc:
begin
setup_header()
send_header(socket)
send_body(socket)
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex
@logger.debug(ex)
@keep_alive = false
rescue Exception => ex
@logger.error(ex)
@keep_alive = false
end
end
##
# Sets up the headers for sending
def setup_header() # :nodoc:
@reason_phrase ||= HTTPStatus::reason_phrase(@status)
@header['server'] ||= @config[:ServerSoftware]
@header['date'] ||= Time.now.httpdate
# HTTP/0.9 features
if @request_http_version < "1.0"
@http_version = HTTPVersion.new("0.9")
@keep_alive = false
end
# HTTP/1.0 features
if @request_http_version < "1.1"
if chunked?
@chunked = false
ver = @request_http_version.to_s
msg = "chunked is set for an HTTP/#{ver} request. (ignored)"
@logger.warn(msg)
end
end
# Determine the message length (RFC2616 -- 4.4 Message Length)
if @status == 304 || @status == 204 || HTTPStatus::info?(@status)
@header.delete('content-length')
@body = ""
elsif chunked?
@header["transfer-encoding"] = "chunked"
@header.delete('content-length')
elsif %r{^multipart/byteranges} =~ @header['content-type']
@header.delete('content-length')
elsif @header['content-length'].nil?
unless @body.is_a?(IO)
@header['content-length'] = @body ? @body.bytesize : 0
end
end
# Keep-Alive connection.
if @header['connection'] == "close"
@keep_alive = false
elsif keep_alive?
if chunked? || @header['content-length'] || @status == 304 || @status == 204 || HTTPStatus.info?(@status)
@header['connection'] = "Keep-Alive"
else
msg = "Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true"
@logger.warn(msg)
@header['connection'] = "close"
@keep_alive = false
end
else
@header['connection'] = "close"
end
# Location is a single absoluteURI.
if location = @header['location']
if @request_uri
@header['location'] = @request_uri.merge(location)
end
end
end
##
# Sends the headers on +socket+
def send_header(socket) # :nodoc:
if @http_version.major > 0
data = status_line()
@header.each{|key, value|
tmp = key.gsub(/\bwww|^te$|\b\w/){ $&.upcase }
data << "#{tmp}: #{value}" << CRLF
}
@cookies.each{|cookie|
data << "Set-Cookie: " << cookie.to_s << CRLF
}
data << CRLF
_write_data(socket, data)
end
end
##
# Sends the body on +socket+
def send_body(socket) # :nodoc:
if @body.respond_to? :readpartial then
send_body_io(socket)
elsif @body.respond_to?(:call) then
send_body_proc(socket)
else
send_body_string(socket)
end
end
def to_s # :nodoc:
ret = ""
send_response(ret)
ret
end
##
# Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+.
#
# Example:
#
# res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect
def set_redirect(status, url)
@body = "<HTML><A HREF=\"#{url}\">#{url}</A>.</HTML>\n"
@header['location'] = url.to_s
raise status
end
##
# Creates an error page for exception +ex+ with an optional +backtrace+
def set_error(ex, backtrace=false)
case ex
when HTTPStatus::Status
@keep_alive = false if HTTPStatus::error?(ex.code)
self.status = ex.code
else
@keep_alive = false
self.status = HTTPStatus::RC_INTERNAL_SERVER_ERROR
end
@header['content-type'] = "text/html; charset=ISO-8859-1"
if respond_to?(:create_error_page)
create_error_page()
return
end
if @request_uri
host, port = @request_uri.host, @request_uri.port
else
host, port = @config[:ServerName], @config[:Port]
end
error_body(backtrace, ex, host, port)
end
private
# :stopdoc:
def error_body(backtrace, ex, host, port)
@body = ''
@body << <<-_end_of_html_
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<HTML>
<HEAD><TITLE>#{HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD>
<BODY>
<H1>#{HTMLUtils::escape(@reason_phrase)}</H1>
#{HTMLUtils::escape(ex.message)}
<HR>
_end_of_html_
if backtrace && $DEBUG
@body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' "
@body << "#{HTMLUtils::escape(ex.message)}"
@body << "<PRE>"
ex.backtrace.each{|line| @body << "\t#{line}\n"}
@body << "</PRE><HR>"
end
@body << <<-_end_of_html_
<ADDRESS>
#{HTMLUtils::escape(@config[:ServerSoftware])} at
#{host}:#{port}
</ADDRESS>
</BODY>
</HTML>
_end_of_html_
end
def send_body_io(socket)
begin
if @request_method == "HEAD"
# do nothing
elsif chunked?
buf = ''
begin
@body.readpartial(@buffer_size, buf)
size = buf.bytesize
data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
_write_data(socket, data)
data.clear
@sent_size += size
rescue EOFError
break
end while true
buf.clear
_write_data(socket, "0#{CRLF}#{CRLF}")
else
size = @header['content-length'].to_i
_send_file(socket, @body, 0, size)
@sent_size = size
end
ensure
@body.close
end
end
def send_body_string(socket)
if @request_method == "HEAD"
# do nothing
elsif chunked?
body ? @body.bytesize : 0
while buf = @body[@sent_size, @buffer_size]
break if buf.empty?
size = buf.bytesize
data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
buf.clear
_write_data(socket, data)
@sent_size += size
end
_write_data(socket, "0#{CRLF}#{CRLF}")
else
if @body && @body.bytesize > 0
_write_data(socket, @body)
@sent_size = @body.bytesize
end
end
end
def send_body_proc(socket)
if @request_method == "HEAD"
# do nothing
elsif chunked?
@body.call(ChunkedWrapper.new(socket, self))
_write_data(socket, "0#{CRLF}#{CRLF}")
else
size = @header['content-length'].to_i
@body.call(socket)
@sent_size = size
end
end
class ChunkedWrapper
def initialize(socket, resp)
@socket = socket
@resp = resp
end
def write(buf)
return if buf.empty?
socket = @socket
@resp.instance_eval {
size = buf.bytesize
data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
_write_data(socket, data)
data.clear
@sent_size += size
}
end
alias :<< :write
end
def _send_file(output, input, offset, size)
while offset > 0
sz = @buffer_size < size ? @buffer_size : size
buf = input.read(sz)
offset -= buf.bytesize
end
if size == 0
while buf = input.read(@buffer_size)
_write_data(output, buf)
end
else
while size > 0
sz = @buffer_size < size ? @buffer_size : size
buf = input.read(sz)
_write_data(output, buf)
size -= buf.bytesize
end
end
end
def _write_data(socket, data)
socket << data
end
# :startdoc:
end
end