lib/roda/response.rb
# frozen-string-literal: true begin require 'rack/headers' rescue LoadError end class Roda # Contains constants for response headers. This approach is used so that all # headers used internally by Roda can be lower case on Rack 3, so that it is # possible to use a plain hash of response headers instead of using Rack::Headers. module RodaResponseHeaders downcase = defined?(Rack::Headers) && Rack::Headers.is_a?(Class) %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length Content-Security-Policy Content-Security-Policy-Report-Only Content-Type ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary Permissions-Policy Permissions-Policy-Report-Only Strict-Transport-Security'. each do |value| value = value.downcase if downcase const_set(value.gsub('-', '_').upcase!.to_sym, value.freeze) end end # Base class used for Roda responses. The instance methods for this # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods. class RodaResponse @roda_class = ::Roda end module RodaPlugins module Base # Class methods for RodaResponse module ResponseClassMethods # Reference to the Roda class related to this response class. attr_accessor :roda_class # Since RodaResponse is anonymously subclassed when Roda is subclassed, # and then assigned to a constant of the Roda subclass, make inspect # reflect the likely name for the class. def inspect "#{roda_class.inspect}::RodaResponse" end end # Instance methods for RodaResponse module ResponseMethods DEFAULT_HEADERS = {RodaResponseHeaders::CONTENT_TYPE => "text/html".freeze}.freeze # The body for the current response. attr_reader :body # The hash of response headers for the current response. attr_reader :headers # The status code to use for the response. If none is given, will use 200 # code for non-empty responses and a 404 code for empty responses. attr_accessor :status # Set the default headers when creating a response. def initialize @headers = _initialize_headers @body = [] @length = 0 end # Return the response header with the given key. Example: # # response['Content-Type'] # => 'text/html' def [](key) @headers[key] end # Set the response header with the given key to the given value. # # response['Content-Type'] = 'application/json' def []=(key, value) @headers[key] = value end # The default headers to use for responses. def default_headers DEFAULT_HEADERS end # Whether the response body has been written to yet. Note # that writing an empty string to the response body marks # the response as not empty. Example: # # response.empty? # => true # response.write('a') # response.empty? # => false def empty? @body.empty? end # Return the rack response array of status, headers, and body # for the current response. If the status has not been set, # uses the return value of default_status if the body has # been written to, otherwise uses a 404 status. # Adds the Content-Length header to the size of the response body. # # Example: # # response.finish # # => [200, # # {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, # # []] def finish b = @body set_default_headers h = @headers if b.empty? s = @status || 404 if (s == 304 || s == 204 || (s >= 100 && s <= 199)) h.delete(RodaResponseHeaders::CONTENT_TYPE) elsif s == 205 empty_205_headers(h) else h[RodaResponseHeaders::CONTENT_LENGTH] ||= '0' end else s = @status || default_status h[RodaResponseHeaders::CONTENT_LENGTH] ||= @length.to_s end [s, h, b] end # Return the rack response array using a given body. Assumes a # 200 response status unless status has been explicitly set, # and doesn't add the Content-Length header or use the existing # body. def finish_with_body(body) set_default_headers [@status || default_status, @headers, body] end # Return the default response status to be used when the body # has been written to. This is split out to make overriding # easier in plugins. def default_status 200 end # Show response class, status code, response headers, and response body def inspect "#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>" end # Set the Location header to the given path, and the status # to the given status. Example: # # response.redirect('foo', 301) # response.redirect('bar') def redirect(path, status = 302) @headers[RodaResponseHeaders::LOCATION] = path @status = status nil end # Return the Roda class related to this response. def roda_class self.class.roda_class end # Write to the response body. Returns nil. # # response.write('foo') def write(str) s = str.to_s @length += s.bytesize @body << s nil end private if defined?(Rack::Headers) && Rack::Headers.is_a?(Class) # Use Rack::Headers for headers by default on Rack 3 def _initialize_headers Rack::Headers.new end else # Use plain hash for headers by default on Rack 1-2 def _initialize_headers {} end end if Rack.release < '2.0.2' # Don't use a content length for empty 205 responses on # rack 1, as it violates Rack::Lint in that version. def empty_205_headers(headers) headers.delete(RodaResponseHeaders::CONTENT_TYPE) headers.delete(RodaResponseHeaders::CONTENT_LENGTH) end else # Set the content length for empty 205 responses to 0 def empty_205_headers(headers) headers.delete(RodaResponseHeaders::CONTENT_TYPE) headers[RodaResponseHeaders::CONTENT_LENGTH] = '0' end end # For each default header, if a header has not already been set for the # response, set the header in the response. def set_default_headers h = @headers default_headers.each do |k,v| h[k] ||= v end end end end end end