rev: async response bodies with DevFdResponse middleware
[rainbows.git] / lib / rainbows / dev_fd_response.rb
blobe4e5f0c41900fbb4853fd58e4a1783cc90346ec7
1 # -*- encoding: binary -*-
3 module Rainbows
5   # Rack response middleware wrapping any IO-like object with an
6   # OS-level file descriptor associated with it.  May also be used to
7   # create responses from integer file descriptors or existing +IO+
8   # objects.  This may be used in conjunction with the #to_path method
9   # on servers that support it to pass arbitrary file descriptors into
10   # the HTTP response without additional open(2) syscalls
12   class DevFdResponse < Struct.new(:app, :to_io, :to_path)
13     include Rack::Utils
15     # Rack middleware entry point, we'll just pass through responses
16     # unless they respond to +to_io+ or +to_path+
17     def call(env)
18       status, headers, body = response = app.call(env)
20       # totally uninteresting to us if there's no body
21       return response if STATUS_WITH_NO_ENTITY_BODY.include?(status)
23       io = body.to_io if body.respond_to?(:to_io)
24       io ||= File.open(body.to_path, 'rb') if body.respond_to?(:to_path)
25       return response if io.nil?
27       headers = HeaderHash.new(headers)
28       st = io.stat
29       if st.file?
30         headers['Content-Length'] ||= st.size.to_s
31         headers.delete('Transfer-Encoding')
32       elsif st.pipe? || st.socket? # epoll-able things
33         if env['rainbows.autochunk']
34           headers['Transfer-Encoding'] = 'chunked'
35           headers.delete('Content-Length')
36         else
37           headers['X-Rainbows-Autochunk'] = 'no'
38         end
39       else # unlikely, char/block device file, directory, ...
40         return response
41       end
42       resp = dup # be reentrant here
43       resp.to_path = "/dev/fd/#{io.fileno}"
44       resp.to_io = io
45       [ status, headers.to_hash, resp ]
46     end
48     # called by the webserver or other middlewares if they can't
49     # handle #to_path
50     def each(&block)
51       to_io.each(&block)
52     end
54     # remain Rack::Lint-compatible for people with wonky systems :P
55     unless File.exist?("/dev/fd/0")
56       alias to_path_orig to_path
57       undef_method :to_path
58     end
60     # called by the web server after #each
61     def close
62       begin
63         to_io.close if to_io.respond_to?(:close)
64       rescue IOError # could've been IO::new()'ed and closed
65       end
66     end
68   end # class
69 end