Module: Puma::Request

Includes:
Const
Included in:
Server
Defined in:
lib/puma/request.rb

Overview

The methods here are included in Server, but are separated into this file. All the methods here pertain to passing the request to the app, then writing the response back to the client.

None of the methods here are called externally, with the exception of #handle_request, which is called in Server#process_client.

Version:

  • 5.0.3

Constant Summary collapse

BODY_LEN_MAX =

Single element array body: smaller bodies are written to io_buffer first, then a single write from io_buffer. Larger sizes are written separately. Also fixes max size of chunked file body read.

1_024 * 256
IO_BODY_MAX =

File body: smaller bodies are combined with io_buffer, then written to socket. Larger bodies are written separately using ‘copy_stream`

1_024 * 64
IO_BUFFER_LEN_MAX =

Array body: elements are collected in io_buffer. When io_buffer’s size exceeds value, they are written to the socket.

1_024 * 512
SOCKET_WRITE_ERR_MSG =
"Socket timeout writing data"
CUSTOM_STAT =
'CUSTOM'

Constants included from Const

Const::BANNED_HEADER_KEY, Const::CGI_VER, Const::CHUNKED, Const::CHUNK_SIZE, Const::CLOSE, Const::CLOSE_CHUNKED, Const::CODE_NAME, Const::COLON, Const::CONNECTION_CLOSE, Const::CONNECTION_KEEP_ALIVE, Const::CONTENT_LENGTH, Const::CONTENT_LENGTH2, Const::CONTENT_LENGTH_S, Const::CONTINUE, Const::DQUOTE, Const::EARLY_HINTS, Const::ERROR_RESPONSE, Const::FAST_TRACK_KA_TIMEOUT, Const::GATEWAY_INTERFACE, Const::HALT_COMMAND, Const::HEAD, Const::HIJACK, Const::HIJACK_IO, Const::HIJACK_P, Const::HTTP, Const::HTTPS, Const::HTTPS_KEY, Const::HTTP_10_200, Const::HTTP_11, Const::HTTP_11_100, Const::HTTP_11_200, Const::HTTP_CONNECTION, Const::HTTP_EXPECT, Const::HTTP_HEADER_DELIMITER, Const::HTTP_HOST, Const::HTTP_VERSION, Const::HTTP_X_FORWARDED_FOR, Const::HTTP_X_FORWARDED_PROTO, Const::HTTP_X_FORWARDED_SCHEME, Const::HTTP_X_FORWARDED_SSL, Const::IANA_HTTP_METHODS, Const::ILLEGAL_HEADER_KEY_REGEX, Const::ILLEGAL_HEADER_VALUE_REGEX, Const::KEEP_ALIVE, Const::LINE_END, Const::LOCALHOST, Const::LOCALHOST_IPV4, Const::LOCALHOST_IPV6, Const::MAX_BODY, Const::MAX_HEADER, Const::NEWLINE, Const::PATH_INFO, Const::PORT_443, Const::PORT_80, Const::PROXY_PROTOCOL_V1_REGEX, Const::PUMA_CONFIG, Const::PUMA_PEERCERT, Const::PUMA_SERVER_STRING, Const::PUMA_SOCKET, Const::PUMA_TMP_BASE, Const::PUMA_VERSION, Const::QUERY_STRING, Const::RACK_AFTER_REPLY, Const::RACK_INPUT, Const::RACK_URL_SCHEME, Const::REMOTE_ADDR, Const::REQUEST_METHOD, Const::REQUEST_PATH, Const::REQUEST_URI, Const::RESTART_COMMAND, Const::SERVER_NAME, Const::SERVER_PORT, Const::SERVER_PROTOCOL, Const::SERVER_SOFTWARE, Const::STOP_COMMAND, Const::SUPPORTED_HTTP_METHODS, Const::TRANSFER_ENCODING, Const::TRANSFER_ENCODING2, Const::TRANSFER_ENCODING_CHUNKED, Const::UNMASKABLE_HEADERS, Const::UNSPECIFIED_IPV4, Const::UNSPECIFIED_IPV6, Const::WRITE_TIMEOUT

Instance Method Summary collapse

Instance Method Details

#default_server_port(env) ⇒ Puma::Const::PORT_443, Puma::Const::PORT_80

Parameters:

  • env (Hash)

    see Puma::Client#env, from request

Returns:



272
273
274
275
276
277
278
# File 'lib/puma/request.rb', line 272

def default_server_port(env)
  if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
    PORT_443
  else
    PORT_80
  end
end

#handle_request(client, requests) ⇒ Boolean, :async

Takes the request contained in client, invokes the Rack application to construct the response and writes it back to client.io.

It’ll return false when the connection is closed, this doesn’t mean that the response wasn’t successful.

It’ll return :async if the connection remains open but will be handled elsewhere, i.e. the connection has been hijacked by the Rack application.

Finally, it’ll return true on keep-alive connections.

Parameters:

Returns:

  • (Boolean, :async)


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/puma/request.rb', line 50

def handle_request(client, requests)
  env = client.env
  io_buffer = client.io_buffer
  socket  = client.io   # io may be a MiniSSL::Socket
  app_body = nil

  return false if closed_socket?(socket)

  if client.http_content_length_limit_exceeded
    return prepare_response(413, {}, ["Payload Too Large"], requests, client)
  end

  normalize_env env, client

  env[PUMA_SOCKET] = socket

  if env[HTTPS_KEY] && socket.peercert
    env[PUMA_PEERCERT] = socket.peercert
  end

  env[HIJACK_P] = true
  env[HIJACK] = client

  env[RACK_INPUT] = client.body
  env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP

  if @early_hints
    env[EARLY_HINTS] = lambda { |headers|
      begin
        unless (str = str_early_hints headers).empty?
          fast_write_str socket, "HTTP/1.1 103 Early Hints\r\n#{str}\r\n"
        end
      rescue ConnectionError => e
        @log_writer.debug_error e
        # noop, if we lost the socket we just won't send the early hints
      end
    }
  end

  req_env_post_parse env

  # A rack extension. If the app writes #call'ables to this
  # array, we will invoke them when the request is done.
  #
  env[RACK_AFTER_REPLY] ||= []

  begin
    if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
      status, headers, app_body = @thread_pool.with_force_shutdown do
        @app.call(env)
      end
    else
      @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
      status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
    end

    # app_body needs to always be closed, hold value in case lowlevel_error
    # is called
    res_body = app_body

    # full hijack, app called env['rack.hijack']
    return :async if client.hijacked

    status = status.to_i

    if status == -1
      unless headers.empty? and res_body == []
        raise "async response must have empty headers and body"
      end

      return :async
    end
  rescue ThreadPool::ForceShutdown => e
    @log_writer.unknown_error e, client, "Rack app"
    @log_writer.log "Detected force shutdown of a thread"

    status, headers, res_body = lowlevel_error(e, env, 503)
  rescue Exception => e
    @log_writer.unknown_error e, client, "Rack app"

    status, headers, res_body = lowlevel_error(e, env, 500)
  end
  prepare_response(status, headers, res_body, requests, client)
ensure
  io_buffer.reset
  uncork_socket client.io
  app_body.close if app_body.respond_to? :close
  client&.tempfile_close
  after_reply = env[RACK_AFTER_REPLY] || []
  begin
    after_reply.each { |o| o.call }
  rescue StandardError => e
    @log_writer.debug_error e
  end unless after_reply.empty?
end

#prepare_response(status, headers, res_body, requests, client) ⇒ Boolean, :async

Assembles the headers and prepares the body for actually sending the response via ‘#fast_write_response`.

Parameters:

  • status (Integer)

    the status returned by the Rack application

  • headers (Hash)

    the headers returned by the Rack application

  • res_body (Array)

    the body returned by the Rack application or a call to ‘Server#lowlevel_error`

  • requests (Integer)

    number of inline requests handled

  • client (Puma::Client)

Returns:

  • (Boolean, :async)

    keep-alive status or ‘:async`



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/puma/request.rb', line 156

def prepare_response(status, headers, res_body, requests, client)
  env = client.env
  socket = client.io
  io_buffer = client.io_buffer

  return false if closed_socket?(socket)

  # Close the connection after a reasonable number of inline requests
  # if the server is at capacity and the listener has a new connection ready.
  # This allows Puma to service connections fairly when the number
  # of concurrent connections exceeds the size of the threadpool.
  force_keep_alive = if @enable_keep_alives
    requests < @max_fast_inline ||
    @thread_pool.busy_threads < @max_threads ||
    !client.listener.to_io.wait_readable(0)
  else
    # Always set force_keep_alive to false if the server has keep-alives not enabled.
    false
  end

  resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)

  close_body = false
  response_hijack = nil
  content_length = resp_info[:content_length]
  keep_alive     = resp_info[:keep_alive]

  if res_body.respond_to?(:each) && !resp_info[:response_hijack]
    # below converts app_body into body, dependent on app_body's characteristics, and
    # content_length will be set if it can be determined
    if !content_length && !resp_info[:transfer_encoding] && status != 204
      if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
          array_body.is_a?(Array)
        body = array_body.compact
        content_length = body.sum(&:bytesize)
      elsif res_body.is_a?(File) && res_body.respond_to?(:size)
        body = res_body
        content_length = body.size
      elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
        body = File.open fn, 'rb'
        content_length = body.size
        close_body = true
      else
        body = res_body
      end
    elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
        File.readable?(fn = res_body.to_path)
      body = File.open fn, 'rb'
      content_length = body.size
      close_body = true
    elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
        res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
      # Sprockets::Asset
      content_length = res_body.bytesize unless content_length
      if (body_str = res_body.to_hash[:source])
        body = [body_str]
      else                           # avoid each and use a File object
        body = File.open fn, 'rb'
        close_body = true
      end
    else
      body = res_body
    end
  else
    # partial hijack, from Rack spec:
    #   Servers must ignore the body part of the response tuple when the
    #   rack.hijack response header is present.
    response_hijack = resp_info[:response_hijack] || res_body
  end

  line_ending = LINE_END

  cork_socket socket

  if resp_info[:no_body]
    # 101 (Switching Protocols) doesn't return here or have content_length,
    # it should be using `response_hijack`
    unless status == 101
      if content_length && status != 204
        io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
      end

      io_buffer << LINE_END
      fast_write_str socket, io_buffer.read_and_reset
      socket.flush
      return keep_alive
    end
  else
    if content_length
      io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
      chunked = false
    elsif !response_hijack && resp_info[:allow_chunked]
      io_buffer << TRANSFER_ENCODING_CHUNKED
      chunked = true
    end
  end

  io_buffer << line_ending

  # partial hijack, we write headers, then hand the socket to the app via
  # response_hijack.call
  if response_hijack
    fast_write_str socket, io_buffer.read_and_reset
    uncork_socket socket
    response_hijack.call socket
    return :async
  end

  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
  body.close if close_body
  keep_alive
end