ev_core: do not autochunk HTTP/1.0 (and 0.9) responses
[rainbows.git] / lib / rainbows / dev_fd_response.rb
blobedc39af7223ab31a0e60722cccc780d7aa2ddf9b
1 # -*- encoding: binary -*-
3 # Rack response middleware wrapping any IO-like object with an
4 # OS-level file descriptor associated with it.  May also be used to
5 # create responses from integer file descriptors or existing +IO+
6 # objects.  This may be used in conjunction with the #to_path method
7 # on servers that support it to pass arbitrary file descriptors into
8 # the HTTP response without additional open(2) syscalls
10 # This middleware is currently a no-op for Rubinius, as it lacks
11 # IO.copy_stream in 1.9 and also due to a bug here:
12 #   http://github.com/evanphx/rubinius/issues/379
14 class Rainbows::DevFdResponse < Struct.new(:app)
16   # :stopdoc:
17   FD_MAP = Rainbows::FD_MAP
18   Content_Length = "Content-Length".freeze
19   Transfer_Encoding = "Transfer-Encoding".freeze
20   Rainbows_autochunk = "rainbows.autochunk".freeze
21   Rainbows_model = "rainbows.model"
22   HTTP_VERSION = "HTTP_VERSION"
23   Chunked = "chunked"
25   # make this a no-op under Rubinius, it's pointless anyways
26   # since Rubinius doesn't have IO.copy_stream
27   def self.new(app)
28     app
29   end if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
30   include Rack::Utils
32   # Rack middleware entry point, we'll just pass through responses
33   # unless they respond to +to_io+ or +to_path+
34   def call(env)
35     status, headers, body = response = app.call(env)
37     # totally uninteresting to us if there's no body
38     if STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) ||
39        File === body ||
40        (body.respond_to?(:to_path) && File.file?(body.to_path))
41       return response
42     end
44     io = body.to_io if body.respond_to?(:to_io)
45     io ||= File.open(body.to_path) if body.respond_to?(:to_path)
46     return response if io.nil?
48     headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
49     st = io.stat
50     fileno = io.fileno
51     FD_MAP[fileno] = io
52     if st.file?
53       headers[Content_Length] ||= st.size.to_s
54       headers.delete(Transfer_Encoding)
55     elsif st.pipe? || st.socket? # epoll-able things
56       unless headers.include?(Content_Length)
57         if env[Rainbows_autochunk]
58           case env[HTTP_VERSION]
59           when "HTTP/1.0", nil
60           else
61             headers[Transfer_Encoding] = Chunked
62           end
63         else
64           env[Rainbows_autochunk] = false
65         end
66       end
68       # we need to make sure our pipe output is Fiber-compatible
69       case env[Rainbows_model]
70       when :FiberSpawn, :FiberPool, :RevFiberSpawn, :CoolioFiberSpawn
71         io.respond_to?(:kgio_wait_readable) or
72           io = Rainbows::Fiber::IO.new(io)
73       when :Revactor
74         io = Rainbows::Revactor::Proxy.new(io)
75       end
76     else # unlikely, char/block device file, directory, ...
77       return response
78     end
79     [ status, headers, Body.new(io, "/dev/fd/#{fileno}", body) ]
80   end
82   class Body < Struct.new(:to_io, :to_path, :orig_body)
83     # called by the webserver or other middlewares if they can't
84     # handle #to_path
85     def each
86       to_io.each { |x| yield x }
87     end
89     # remain Rack::Lint-compatible for people with wonky systems :P
90     unless File.directory?("/dev/fd")
91       alias to_path_orig to_path
92       undef_method :to_path
93     end
95     # called by the web server after #each
96     def close
97       to_io.close unless to_io.closed?
98       orig_body.close if orig_body.respond_to?(:close) # may not be an IO
99     rescue IOError # could've been IO::new()'ed and closed
100     end
101   end
102   #:startdoc:
103 end # class