1 # frozen_string_literal: true
6 require_relative 'constants'
7 require_relative 'utils'
10 # Rack::Lint validates your application and the requests and
11 # responses according to the Rack spec.
14 ALLOWED_SCHEMES = %w(https http wss ws).freeze
16 REQUEST_PATH_ORIGIN_FORM = /\A\/[^#]*\z/
17 REQUEST_PATH_ABSOLUTE_FORM = /\A#{Utils::URI_PARSER.make_regexp}\z/
18 REQUEST_PATH_AUTHORITY_FORM = /\A[^\/:]+:\d+\z/
19 REQUEST_PATH_ASTERISK_FORM = '*'
23 class LintError < RuntimeError; end
24 # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
25 # should not be removed. The whitespace creates paragraphs in the RDoc
28 ## This specification aims to formalize the Rack protocol. You
29 ## can (and should) use Rack::Lint to enforce it.
31 ## When you develop middleware, be sure to add a Lint before and
32 ## after to catch all mistakes.
34 ## = Rack applications
36 ## A Rack application is a Ruby object (not a class) that
37 ## responds to +call+.
39 raise LintError, "app must respond to call" unless app.respond_to?(:call)
45 Wrapper.new(@app, env).response
49 def initialize(app, env)
65 ## It takes exactly one argument, the *environment*
66 raise LintError, "No env given" unless @env
67 check_environment(@env)
69 ## and returns a non-frozen Array of exactly three values:
70 @response = @app.call(@env)
71 raise LintError, "response is not an Array, but #{@response.class}" unless @response.kind_of? Array
72 raise LintError, "response is frozen" if @response.frozen?
73 raise LintError, "response array has #{@response.size} elements instead of 3" unless @response.size == 3
75 @status, @headers, @body = @response
80 check_headers(@headers)
82 hijack_proc = check_hijack_response(@headers, @env)
84 @headers[RACK_HIJACK] = hijack_proc
88 check_content_type_header(@status, @headers)
89 check_content_length_header(@status, @headers)
90 check_rack_protocol_header(@status, @headers)
91 @head_request = @env[REQUEST_METHOD] == HEAD
93 @lint = (@env['rack.lint'] ||= []) << self
95 if (@env['rack.lint.body_iteration'] ||= 0) > 0
96 raise LintError, "Middleware must not call #each directly"
99 return [@status, @headers, self]
103 ## == The Environment
105 def check_environment(env)
106 ## The environment must be an unfrozen instance of Hash that includes
107 ## CGI-like headers. The Rack application is free to modify the
109 raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash
110 raise LintError, "env should not be frozen, but is" if env.frozen?
113 ## The environment is required to include these variables
114 ## (adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see
117 ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
118 ## "GET" or "POST". This cannot ever
119 ## be an empty string, and so is
122 ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
123 ## URL's "path" that corresponds to the
124 ## application object, so that the
125 ## application knows its virtual
126 ## "location". This may be an empty
127 ## string, if the application corresponds
128 ## to the "root" of the server.
130 ## <tt>PATH_INFO</tt>:: The remainder of the request URL's
131 ## "path", designating the virtual
132 ## "location" of the request's target
133 ## within the application. This may be an
134 ## empty string, if the request URL targets
135 ## the application root and does not have a
136 ## trailing slash. This value may be
137 ## percent-encoded when originating from
140 ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
141 ## follows the <tt>?</tt>, if any. May be
142 ## empty, but is always required!
144 ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
145 ## <tt>PATH_INFO</tt>, these variables can be
146 ## used to complete the URL. Note, however,
147 ## that <tt>HTTP_HOST</tt>, if present,
148 ## should be used in preference to
149 ## <tt>SERVER_NAME</tt> for reconstructing
151 ## <tt>SERVER_NAME</tt> can never be an empty
152 ## string, and so is always required.
154 ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
155 ## server is running on. Should be specified if
156 ## the server is running on a non-standard port.
158 ## <tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used
161 ## <tt>HTTP_</tt> Variables:: Variables corresponding to the
162 ## client-supplied HTTP request
163 ## headers (i.e., variables whose
164 ## names begin with <tt>HTTP_</tt>). The
165 ## presence or absence of these
166 ## variables should correspond with
167 ## the presence or absence of the
168 ## appropriate HTTP header in the
170 ## {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
171 ## for specific behavior.
173 ## In addition to this, the Rack environment must include these
174 ## Rack-specific variables:
176 ## <tt>rack.url_scheme</tt>:: The scheme of the incoming request, must
177 ## be one of +http+, +https+, +ws+ or +wss+.
179 ## <tt>rack.input</tt>:: See below, the input stream.
181 ## <tt>rack.errors</tt>:: See below, the error stream.
183 ## <tt>rack.hijack?</tt>:: See below, if present and true, indicates
184 ## that the server supports partial hijacking.
186 ## <tt>rack.hijack</tt>:: See below, if present, an object responding
187 ## to +call+ that is used to perform a full
190 ## <tt>rack.protocol</tt>:: An optional +Array+ of +String+, containing
191 ## the protocols advertised by the client in
192 ## the +upgrade+ header (HTTP/1) or the
193 ## +:protocol+ pseudo-header (HTTP/2).
194 if protocols = @env['rack.protocol']
195 unless protocols.is_a?(Array) && protocols.all?{|protocol| protocol.is_a?(String)}
196 raise LintError, "rack.protocol must be an Array of Strings"
200 ## Additional environment specifications have approved to
201 ## standardized middleware APIs. None of these are required to
202 ## be implemented by the server.
204 ## <tt>rack.session</tt>:: A hash-like interface for storing
205 ## request session data.
206 ## The store must implement:
207 if session = env[RACK_SESSION]
208 ## store(key, value) (aliased as []=);
209 unless session.respond_to?(:store) && session.respond_to?(:[]=)
210 raise LintError, "session #{session.inspect} must respond to store and []="
213 ## fetch(key, default = nil) (aliased as []);
214 unless session.respond_to?(:fetch) && session.respond_to?(:[])
215 raise LintError, "session #{session.inspect} must respond to fetch and []"
219 unless session.respond_to?(:delete)
220 raise LintError, "session #{session.inspect} must respond to delete"
224 unless session.respond_to?(:clear)
225 raise LintError, "session #{session.inspect} must respond to clear"
228 ## to_hash (returning unfrozen Hash instance);
229 unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
230 raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance"
234 ## <tt>rack.logger</tt>:: A common object interface for logging messages.
235 ## The object must implement:
236 if logger = env[RACK_LOGGER]
237 ## info(message, &block)
238 unless logger.respond_to?(:info)
239 raise LintError, "logger #{logger.inspect} must respond to info"
242 ## debug(message, &block)
243 unless logger.respond_to?(:debug)
244 raise LintError, "logger #{logger.inspect} must respond to debug"
247 ## warn(message, &block)
248 unless logger.respond_to?(:warn)
249 raise LintError, "logger #{logger.inspect} must respond to warn"
252 ## error(message, &block)
253 unless logger.respond_to?(:error)
254 raise LintError, "logger #{logger.inspect} must respond to error"
257 ## fatal(message, &block)
258 unless logger.respond_to?(:fatal)
259 raise LintError, "logger #{logger.inspect} must respond to fatal"
263 ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
264 if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
265 unless bufsize.is_a?(Integer) && bufsize > 0
266 raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified"
270 ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
271 if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
272 raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call)
273 env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
274 io = tempfile_factory.call(filename, content_type)
275 raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<)
280 ## The server or the application can store their own data in the
281 ## environment, too. The keys must contain at least one dot,
282 ## and should be prefixed uniquely. The prefix <tt>rack.</tt>
283 ## is reserved for use with the Rack core distribution and other
284 ## accepted specifications and must not be used otherwise.
286 %w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL rack.errors].each do |header|
287 raise LintError, "env missing required key #{header}" unless env.include? header
290 ## The <tt>SERVER_PORT</tt> must be an Integer if set.
291 server_port = env["SERVER_PORT"]
292 unless server_port.nil? || (Integer(server_port) rescue false)
293 raise LintError, "env[SERVER_PORT] is not an Integer"
296 ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
297 unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false)
298 raise LintError, "#{env[SERVER_NAME]} must be a valid authority"
301 ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
302 unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false)
303 raise LintError, "#{env[HTTP_HOST]} must be a valid authority"
306 ## The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>.
307 server_protocol = env['SERVER_PROTOCOL']
308 unless %r{HTTP/\d(\.\d)?}.match?(server_protocol)
309 raise LintError, "env[SERVER_PROTOCOL] does not match HTTP/\\d(\\.\\d)?"
312 ## The environment must not contain the keys
313 ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
314 ## (use the versions without <tt>HTTP_</tt>).
315 %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
316 if env.include? header
317 raise LintError, "env contains #{header}, must use #{header[5..-1]}"
321 ## The CGI keys (named without a period) must have String values.
322 ## If the string values for CGI keys contain non-ASCII characters,
323 ## they should use ASCII-8BIT encoding.
324 env.each { |key, value|
325 next if key.include? "." # Skip extensions
326 unless value.kind_of? String
327 raise LintError, "env variable #{key} has non-string value #{value.inspect}"
329 next if value.encoding == Encoding::ASCII_8BIT
330 unless value.b !~ /[\x80-\xff]/n
331 raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}"
335 ## There are the following restrictions:
337 ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
338 unless ALLOWED_SCHEMES.include?(env[RACK_URL_SCHEME])
339 raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
342 ## * There may be a valid input stream in <tt>rack.input</tt>.
343 if rack_input = env[RACK_INPUT]
344 check_input_stream(rack_input)
345 @env[RACK_INPUT] = InputWrapper.new(rack_input)
348 ## * There must be a valid error stream in <tt>rack.errors</tt>.
349 rack_errors = env[RACK_ERRORS]
350 check_error_stream(rack_errors)
351 @env[RACK_ERRORS] = ErrorWrapper.new(rack_errors)
353 ## * There may be a valid hijack callback in <tt>rack.hijack</tt>
355 ## * There may be a valid early hints callback in <tt>rack.early_hints</tt>
356 check_early_hints env
358 ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
359 unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
360 raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}"
363 ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
364 if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
365 raise LintError, "SCRIPT_NAME must start with /"
368 ## * The <tt>PATH_INFO</tt>, if provided, must be a valid request target or an empty string.
369 if env.include?(PATH_INFO)
371 when REQUEST_PATH_ASTERISK_FORM
372 ## * Only <tt>OPTIONS</tt> requests may have <tt>PATH_INFO</tt> set to <tt>*</tt> (asterisk-form).
373 unless env[REQUEST_METHOD] == OPTIONS
374 raise LintError, "Only OPTIONS requests may have PATH_INFO set to '*' (asterisk-form)"
376 when REQUEST_PATH_AUTHORITY_FORM
377 ## * Only <tt>CONNECT</tt> requests may have <tt>PATH_INFO</tt> set to an authority (authority-form). Note that in HTTP/2+, the authority-form is not a valid request target.
378 unless env[REQUEST_METHOD] == CONNECT
379 raise LintError, "Only CONNECT requests may have PATH_INFO set to an authority (authority-form)"
381 when REQUEST_PATH_ABSOLUTE_FORM
382 ## * <tt>CONNECT</tt> and <tt>OPTIONS</tt> requests must not have <tt>PATH_INFO</tt> set to a URI (absolute-form).
383 if env[REQUEST_METHOD] == CONNECT || env[REQUEST_METHOD] == OPTIONS
384 raise LintError, "CONNECT and OPTIONS requests must not have PATH_INFO set to a URI (absolute-form)"
386 when REQUEST_PATH_ORIGIN_FORM
387 ## * Otherwise, <tt>PATH_INFO</tt> must start with a <tt>/</tt> and must not include a fragment part starting with '#' (origin-form).
389 # Empty string is okay.
391 raise LintError, "PATH_INFO must start with a '/' and must not include a fragment part starting with '#' (origin-form)"
395 ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
396 if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
397 raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
400 ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
401 ## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
402 ## <tt>SCRIPT_NAME</tt> is empty.
403 unless env[SCRIPT_NAME] || env[PATH_INFO]
404 raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)"
406 ## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
407 unless env[SCRIPT_NAME] != "/"
408 raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
411 ## <tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
412 ## processed. This would typically be invoked after sending the response to
413 ## the client, but it could also be invoked if an error occurs while
414 ## generating the response or sending the response; in that case, the error
415 ## argument will be a subclass of +Exception+.
416 ## The callables are invoked with +env, status, headers, error+ arguments and
417 ## should not raise any exceptions. They should be invoked in reverse order
419 if callables = env[RACK_RESPONSE_FINISHED]
420 raise LintError, "rack.response_finished must be an array of callable objects" unless callables.is_a?(Array)
422 callables.each do |callable|
423 raise LintError, "rack.response_finished values must respond to call(env, status, headers, error)" unless callable.respond_to?(:call)
429 ## === The Input Stream
431 ## The input stream is an IO-like object which contains the raw HTTP
433 def check_input_stream(input)
434 ## When applicable, its external encoding must be "ASCII-8BIT" and it
435 ## must be opened in binary mode.
436 if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
437 raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
439 if input.respond_to?(:binmode?) && !input.binmode?
440 raise LintError, "rack.input #{input} is not opened in binary mode"
443 ## The input stream must respond to +gets+, +each+, and +read+.
444 [:gets, :each, :read].each { |method|
445 unless input.respond_to? method
446 raise LintError, "rack.input #{input} does not respond to ##{method}"
452 def initialize(input)
456 ## * +gets+ must be called without arguments and return a string,
459 raise LintError, "rack.input#gets called with arguments" unless args.size == 0
461 unless v.nil? or v.kind_of? String
462 raise LintError, "rack.input#gets didn't return a String"
467 ## * +read+ behaves like <tt>IO#read</tt>.
468 ## Its signature is <tt>read([length, [buffer]])</tt>.
470 ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
471 ## and +buffer+ must be a String and may not be nil.
473 ## If +length+ is given and not nil, then this method reads at most
474 ## +length+ bytes from the input stream.
476 ## If +length+ is not given or nil, then this method reads
477 ## all data until EOF.
479 ## When EOF is reached, this method returns nil if +length+ is given
480 ## and not nil, or "" if +length+ is not given or is nil.
482 ## If +buffer+ is given, then the read data will be placed
483 ## into +buffer+ instead of a newly created String object.
485 unless args.size <= 2
486 raise LintError, "rack.input#read called with too many arguments"
489 unless args.first.kind_of?(Integer) || args.first.nil?
490 raise LintError, "rack.input#read called with non-integer and non-nil length"
492 unless args.first.nil? || args.first >= 0
493 raise LintError, "rack.input#read called with a negative length"
497 unless args[1].kind_of?(String)
498 raise LintError, "rack.input#read called with non-String buffer"
502 v = @input.read(*args)
504 unless v.nil? or v.kind_of? String
505 raise LintError, "rack.input#read didn't return nil or a String"
509 raise LintError, "rack.input#read(nil) returned nil on EOF"
516 ## * +each+ must be called without arguments and only yield Strings.
518 raise LintError, "rack.input#each called with arguments" unless args.size == 0
520 unless line.kind_of? String
521 raise LintError, "rack.input#each didn't yield a String"
527 ## * +close+ can be called on the input stream to indicate that
528 ## any remaining input is not needed.
535 ## === The Error Stream
537 def check_error_stream(error)
538 ## The error stream must respond to +puts+, +write+ and +flush+.
539 [:puts, :write, :flush].each { |method|
540 unless error.respond_to? method
541 raise LintError, "rack.error #{error} does not respond to ##{method}"
547 def initialize(error)
551 ## * +puts+ must be called with a single argument that responds to +to_s+.
556 ## * +write+ must be called with a single argument that is a String.
558 raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String
562 ## * +flush+ must be called without arguments and must be called
563 ## in order to make the error appear for sure.
568 ## * +close+ must never be called on the error stream.
570 raise LintError, "rack.errors#close must not be called"
577 ## The hijacking interfaces provides a means for an application to take
578 ## control of the HTTP connection. There are two distinct hijack
579 ## interfaces: full hijacking where the application takes over the raw
580 ## connection, and partial hijacking where the application takes over
581 ## just the response body stream. In both cases, the application is
582 ## responsible for closing the hijacked stream.
584 ## Full hijacking only works with HTTP/1. Partial hijacking is functionally
585 ## equivalent to streaming bodies, and is still optionally supported for
586 ## backwards compatibility with older Rack versions.
590 ## Full hijack is used to completely take over an HTTP/1 connection. It
591 ## occurs before any headers are written and causes the request to
592 ## ignores any response generated by the application.
594 ## It is intended to be used when applications need access to raw HTTP/1
597 def check_hijack(env)
598 ## If +rack.hijack+ is present in +env+, it must respond to +call+
599 if original_hijack = env[RACK_HIJACK]
600 raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call)
602 env[RACK_HIJACK] = proc do
603 io = original_hijack.call
605 ## and return an +IO+ instance which can be used to read and write
606 ## to the underlying connection using HTTP/1 semantics and
608 raise LintError, "rack.hijack must return an IO instance" unless io.is_a?(IO)
616 ## ==== Partial Hijack
618 ## Partial hijack is used for bi-directional streaming of the request and
619 ## response body. It occurs after the status and headers are written by
620 ## the server and causes the server to ignore the Body of the response.
622 ## It is intended to be used when applications need bi-directional
625 def check_hijack_response(headers, env)
626 ## If +rack.hijack?+ is present in +env+ and truthy,
627 if env[RACK_IS_HIJACK]
628 ## an application may set the special response header +rack.hijack+
629 if original_hijack = headers[RACK_HIJACK]
630 ## to an object that responds to +call+,
631 unless original_hijack.respond_to?(:call)
632 raise LintError, 'rack.hijack header must respond to #call'
634 ## accepting a +stream+ argument.
636 original_hijack.call StreamWrapper.new(io)
640 ## After the response status and headers have been sent, this hijack
641 ## callback will be invoked with a +stream+ argument which follows the
642 ## same interface as outlined in "Streaming Body". Servers must
643 ## ignore the +body+ part of the response tuple when the
644 ## +rack.hijack+ response header is present. Using an empty +Array+
645 ## instance is recommended.
648 ## The special response header +rack.hijack+ must only be set
649 ## if the request +env+ has a truthy +rack.hijack?+.
650 if headers.key?(RACK_HIJACK)
651 raise LintError, 'rack.hijack header must not be present if server does not support hijacking'
661 ## The application or any middleware may call the <tt>rack.early_hints</tt>
662 ## with an object which would be valid as the headers of a Rack response.
663 def check_early_hints(env)
664 if env[RACK_EARLY_HINTS]
666 ## If <tt>rack.early_hints</tt> is present, it must respond to #call.
667 unless env[RACK_EARLY_HINTS].respond_to?(:call)
668 raise LintError, "rack.early_hints must respond to call"
671 original_callback = env[RACK_EARLY_HINTS]
672 env[RACK_EARLY_HINTS] = lambda do |headers|
673 ## If <tt>rack.early_hints</tt> is called, it must be called with
674 ## valid Rack response headers.
675 check_headers(headers)
676 original_callback.call(headers)
686 def check_status(status)
687 ## This is an HTTP status. It must be an Integer greater than or equal to
689 unless status.is_a?(Integer) && status >= 100
690 raise LintError, "Status must be an Integer >=100"
697 def check_headers(headers)
698 ## The headers must be a unfrozen Hash.
699 unless headers.kind_of?(Hash)
700 raise LintError, "headers object should be a hash, but isn't (got #{headers.class} as headers)"
704 raise LintError, "headers object should not be frozen, but is"
707 headers.each do |key, value|
708 ## The header keys must be Strings.
709 unless key.kind_of? String
710 raise LintError, "header key must be a string, was #{key.class}"
713 ## Special headers starting "rack." are for communicating with the
714 ## server, and must not be sent back to the client.
715 next if key.start_with?("rack.")
717 ## The header must not contain a +Status+ key.
718 raise LintError, "header must not contain status" if key == "status"
719 ## Header keys must conform to RFC7230 token specification, i.e. cannot
720 ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
721 raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/
722 ## Header keys must not contain uppercase ASCII characters (A-Z).
723 raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/
725 ## Header values must be either a String instance,
726 if value.kind_of?(String)
727 check_header_value(key, value)
728 elsif value.kind_of?(Array)
729 ## or an Array of String instances,
730 value.each{|value| check_header_value(key, value)}
732 raise LintError, "a header value must be a String or Array of Strings, but the value of '#{key}' is a #{value.class}"
737 def check_header_value(key, value)
738 ## such that each String instance must not contain characters below 037.
739 if value =~ /[\000-\037]/
740 raise LintError, "invalid header value #{key}: #{value.inspect}"
745 ## ==== The +content-type+ Header
747 def check_content_type_header(status, headers)
748 headers.each { |key, value|
749 ## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
751 if key == "content-type"
752 if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
753 raise LintError, "content-type header found in #{status} response, not allowed"
761 ## ==== The +content-length+ Header
763 def check_content_length_header(status, headers)
764 headers.each { |key, value|
765 if key == 'content-length'
766 ## There must not be a <tt>content-length</tt> header key when the
767 ## +Status+ is 1xx, 204, or 304.
768 if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
769 raise LintError, "content-length header found in #{status} response, not allowed"
771 @content_length = value
776 def verify_content_length(size)
779 raise LintError, "Response body was given for HEAD request, but should be empty"
781 elsif @content_length
782 unless @content_length == size.to_s
783 raise LintError, "content-length header was #{@content_length}, but should be #{size}"
789 ## ==== The +rack.protocol+ Header
791 def check_rack_protocol_header(status, headers)
792 ## If the +rack.protocol+ header is present, it must be a +String+, and
793 ## must be one of the values from the +rack.protocol+ array from the
795 protocol = headers['rack.protocol']
798 request_protocols = @env['rack.protocol']
800 if request_protocols.nil?
801 raise LintError, "rack.protocol header is #{protocol.inspect}, but rack.protocol was not set in request!"
802 elsif !request_protocols.include?(protocol)
803 raise LintError, "rack.protocol header is #{protocol.inspect}, but should be one of #{request_protocols.inspect} from the request!"
808 ## Setting this value informs the server that it should perform a
809 ## connection upgrade. In HTTP/1, this is done using the +upgrade+
810 ## header. In HTTP/2, this is done by accepting the request.
814 ## The Body is typically an +Array+ of +String+ instances, an enumerable
815 ## that yields +String+ instances, a +Proc+ instance, or a File-like
818 ## The Body must respond to +each+ or +call+. It may optionally respond
819 ## to +to_path+ or +to_ary+. A Body that responds to +each+ is considered
820 ## to be an Enumerable Body. A Body that responds to +call+ is considered
821 ## to be a Streaming Body.
823 ## A Body that responds to both +each+ and +call+ must be treated as an
824 ## Enumerable Body, not a Streaming Body. If it responds to +each+, you
825 ## must call +each+ and not +call+. If the Body doesn't respond to
826 ## +each+, then you can assume it responds to +call+.
828 ## The Body must either be consumed or returned. The Body is consumed by
829 ## optionally calling either +each+ or +call+.
830 ## Then, if the Body responds to +close+, it must be called to release
831 ## any resources associated with the generation of the body.
832 ## In other words, +close+ must always be called at least once; typically
833 ## after the web server has sent the response to the client, but also in
834 ## cases where the Rack application makes internal/virtual requests and
835 ## discards the response.
839 ## After calling +close+, the Body is considered closed and should not
840 ## be consumed again.
843 ## If the original Body is replaced by a new Body, the new Body must
844 ## also consume the original Body by calling +close+ if possible.
845 @body.close if @body.respond_to?(:close)
847 index = @lint.index(self)
848 unless @env['rack.lint'][0..index].all? {|lint| lint.instance_variable_get(:@closed)}
849 raise LintError, "Body has not been closed"
855 ## If the Body responds to +to_path+, it must return a +String+
856 ## path for the local file system whose contents are identical
857 ## to that produced by calling +each+; this may be used by the
858 ## server as an alternative, possibly more efficient way to
859 ## transport the response. The +to_path+ method does not consume
861 if @body.respond_to?(:to_path)
862 unless ::File.exist? @body.to_path
863 raise LintError, "The file identified by body.to_path does not exist"
869 ## ==== Enumerable Body
872 ## The Enumerable Body must respond to +each+.
873 raise LintError, "Enumerable Body must respond to each" unless @body.respond_to?(:each)
875 ## It must only be called once.
876 raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
878 ## It must not be called after being closed,
879 raise LintError, "Response body is already closed" if @closed
883 @body.each do |chunk|
884 ## and must only yield String values.
885 unless chunk.kind_of? String
886 raise LintError, "Body yielded non-string value #{chunk.inspect}"
890 ## Middleware must not call +each+ directly on the Body.
891 ## Instead, middleware can return a new Body that calls +each+ on the
892 ## original Body, yielding at least once per iteration.
894 @env['rack.lint.body_iteration'] += 1
896 if (@env['rack.lint.body_iteration'] -= 1) > 0
897 raise LintError, "New body must yield at least once per iteration of old body"
901 @size += chunk.bytesize
905 verify_content_length(@size)
910 BODY_METHODS = {to_ary: true, each: true, call: true, to_path: true}
916 def respond_to?(name, *)
917 if BODY_METHODS.key?(name)
918 @body.respond_to?(name)
925 ## If the Body responds to +to_ary+, it must return an +Array+ whose
926 ## contents are identical to that produced by calling +each+.
927 ## Middleware may call +to_ary+ directly on the Body and return a new
928 ## Body in its place. In other words, middleware can only process the
929 ## Body directly if it responds to +to_ary+. If the Body responds to both
930 ## +to_ary+ and +close+, its implementation of +to_ary+ must call
933 @body.to_ary.tap do |content|
934 unless content == @body.enum_for.to_a
935 raise LintError, "#to_ary not identical to contents produced by calling #each"
943 ## ==== Streaming Body
946 ## The Streaming Body must respond to +call+.
947 raise LintError, "Streaming Body must respond to call" unless @body.respond_to?(:call)
949 ## It must only be called once.
950 raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
952 ## It must not be called after being closed.
953 raise LintError, "Response body is already closed" if @closed
957 ## It takes a +stream+ argument.
959 ## The +stream+ argument must implement:
960 ## <tt>read, write, <<, flush, close, close_read, close_write, closed?</tt>
962 @body.call(StreamWrapper.new(stream))
968 ## The semantics of these IO methods must be a best effort match to
969 ## those of a normal Ruby IO or Socket object, using standard arguments
970 ## and raising standard exceptions. Servers are encouraged to simply
971 ## pass on real IO objects, although it is recognized that this approach
972 ## is not directly compatible with HTTP/2.
974 :read, :write, :<<, :flush, :close,
975 :close_read, :close_write, :closed?
978 def_delegators :@stream, *REQUIRED_METHODS
980 def initialize(stream)
983 REQUIRED_METHODS.each do |method_name|
984 raise LintError, "Stream must respond to #{method_name}" unless stream.respond_to?(method_name)
996 ## Some parts of this specification are adopted from {PEP 333 – Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/]
997 ## I'd like to thank everyone involved in that effort.