Skip to content

Commit

Permalink
Fix: rework of HTTP::Server of Crystal 0.11
Browse files Browse the repository at this point in the history
  • Loading branch information
ysbaddaden committed Jan 24, 2016
1 parent d29f53c commit 8cfa856
Show file tree
Hide file tree
Showing 27 changed files with 455 additions and 238 deletions.
4 changes: 2 additions & 2 deletions shard.lock
Expand Up @@ -2,13 +2,13 @@ version: 1.0
shards:
minitest:
github: ysbaddaden/minitest.cr
version: 0.2.0
version: 0.3.1

pg:
github: will/crystal-pg
version: 0.5.0

pool:
github: ysbaddaden/pool
version: 0.2.0
version: 0.2.1

7 changes: 4 additions & 3 deletions src/controller.cr
@@ -1,5 +1,5 @@
require "./support/core_ext/http/request"
require "./support/core_ext/http/response"
require "./support/core_ext/http/server/response"
require "./controller/errors"
require "./controller/filtering"
require "./controller/params"
Expand Down Expand Up @@ -88,8 +88,8 @@ module Frost
getter :action_name

# :nodoc:
def initialize(@request, @params, @action_name)
@response = HTTP::Response.new(200, "")
def initialize(context, @params, @action_name)
@request, @response = context.request, context.response
end

# Returns the controller name as an underscored String.
Expand Down Expand Up @@ -141,6 +141,7 @@ module Frost

def run_action
super { yield }
nil
rescue exception
raise exception if rescue_from(exception) == false
end
Expand Down
26 changes: 0 additions & 26 deletions src/controller/cookies.cr

This file was deleted.

5 changes: 3 additions & 2 deletions src/controller/session/test_store.cr
Expand Up @@ -11,8 +11,9 @@ module Frost
CACHE.clear
end

def self.new(request)
new(request, HTTP::Response.new(200), { cookie_name: "_session" })
def self.new(request, response = nil)
response ||= HTTP::Server::Response.new(MemoryIO.new)
new(request, response, { cookie_name: "_session" })
end

def set_data(data)
Expand Down
10 changes: 7 additions & 3 deletions src/controller/test.cr
@@ -1,3 +1,4 @@
require "http/server/context"
require "../support/core_ext/http/headers"
require "./assertions"
require "./session/test_store"
Expand All @@ -24,7 +25,7 @@ module Frost
# delete "/users/1"
# ```
#
# After making a request, you may test the `#response` HTTP::Response
# After making a request, you may test the `#response` HTTP::Client::Response
# object. See `Assertions` for some helpers.
#
# ## Host
Expand Down Expand Up @@ -134,15 +135,18 @@ module Frost
{% end %}

private def execute_request(request, userinfo)
@response = HTTP::Server::Response.new(MemoryIO.new)
context = HTTP::Server::Context.new(request, response)

if @session
Session::TestStore.new(request).set_data(session)
Session::TestStore.new(request, response).set_data(session)
end

if userinfo
request.headers["Authorization"] = "Basic #{Base64.encode(userinfo)}"
end

@response = dispatcher.call(request)
dispatcher.call(context)
@session = Session::TestStore.new(response).read

nil
Expand Down
45 changes: 27 additions & 18 deletions src/dispatcher.cr
Expand Up @@ -11,19 +11,26 @@ module Frost
# rendered, otherwise a 500 Internal Server Error page is rendered. You
# may customize the rendered pages by overloading the `#not_found` and
# `#internal_server_error` methods.
def call(request)
dispatch(request)
rescue ex : Frost::Routing::RoutingError
not_found(request, ex)
rescue ex
Frost.logger.error { "#{ ex.class.name }: #{ ex.message }" }
internal_server_error(request, ex)
def call(context)
begin
dispatch(context)
rescue ex : Frost::Routing::RoutingError
not_found(context, ex)
rescue ex
Frost.logger.error { "#{ ex.class.name }: #{ ex.message }" }
internal_server_error(context, ex)
end

context.response.flush

nil
ensure
Record.release_connection
end

# :nodoc:
def dispatch(request)
def dispatch(context)
request = context.request
params = Frost::Controller::Params.parse(request.query)

if request.headers["Content-Type"]? == "application/x-www-form-urlencoded"
Expand All @@ -36,26 +43,28 @@ module Frost
end
end

_dispatch(request, params)
_dispatch(context, params)
end

# Dispatches a request to the appropriate controller and action.
#
# This method is usually implemented by Mapper.
abstract def _dispatch(request, params)
abstract def _dispatch(context, params)

# TODO: render a generic 404 page (production)
def not_found(request, ex)
response = HTTP::Response.new(404, "#{ ex.message }\n")
response.headers["Content-Type"] = "text/plain"
response
def not_found(context, ex)
response = context.response
response.status_code = 404
response.content_type = "text/plain"
response.body = "#{ ex.message }\n"
end

# TODO: render a generic 500 page (production)
def internal_server_error(request, ex)
response = HTTP::Response.new(500, "#{ ex.class.name }: #{ ex.message }\n#{ ex.backtrace.join("\n") }")
response.headers["Content-Type"] = "text/plain"
response
def internal_server_error(context, ex)
response = context.response
response.status_code = 500
response.content_type = "text/plain"
response.body = "#{ ex.class.name }: #{ ex.message }\n#{ ex.backtrace.join("\n") }"
end
end
end
50 changes: 14 additions & 36 deletions src/generators/application/main.ecr
@@ -1,42 +1,20 @@
require "http/server"
require "frost/server/handlers/log_handler"
require "frost/server/handlers/public_file_handler"
require "frost/server/handlers/deflate_handler"
require "frost/server/handlers/https_everywhere_handler"
require "option_parser"
require "frost/server"
require "./config/bootstrap"

host = "localhost"
port = 9292
module <%= name.camelcase %>
class Server < Frost::Server
#def handlers
# [
# Frost::Server::LogHandler.new,
# Frost::Server::HttpsEverywhereHandler.new(308),
# Frost::Server::PublicFileHandler.new(File.join(Frost.root, "public"))
# ]
#end

opts = OptionParser.new
opts.on("-b HOST", "--bind=HOST", "Bind to host (defaults to #{ host })") { |value| host = value }
opts.on("-p PORT", "--port=PORT", "Bind to port (defaults to #{ port })") { |value| port = value.to_i }
opts.on("-h", "--help", "Show this help") { puts opts; exit }

begin
opts.parse(ARGV)
rescue ex : OptionParser::InvalidOption
STDERR.puts ex.message
STDERR.puts
STDERR.puts "Available options:"
STDERR.puts opts
exit
end

begin
handlers = [
Frost::Server::LogHandler.new,
Frost::Server::HttpsEverywhereHandler.new(308),
HTTP::DeflateHandler.new,
Frost::Server::PublicFileHandler.new(File.join(Frost.root, "public"))
]
dispatcher = <%= name.camelcase %>::Dispatcher.new

server = HTTP::Server.new(host, port, handlers) do |context|
dispatcher.call(context)
def dispatcher
@dispatcher ||= <%= name.camelcase %>::Dispatcher.new
end
end

puts "Listening on http://#{ host }:#{ port }"
server.listen
Server.run
end
5 changes: 3 additions & 2 deletions src/routing/mapper.cr
Expand Up @@ -220,7 +220,8 @@ module Frost
end

io << "class Dispatcher < Frost::Dispatcher\n"
io << " def _dispatch(request, params)\n"
io << " def _dispatch(context, params)\n"
io << " request = context.request\n"

if aggregate.any?
io << " case request.method.upcase\n"
Expand All @@ -233,7 +234,7 @@ module Frost
io << " end\n\n"

io << " if controller\n"
io << " controller.response\n"
io << " context\n"
io << " else\n"
io << " raise Frost::Routing::RoutingError.new(\"No route for \#{ request.method.upcase } \#{ request.path.inspect }\")\n"
io << " end\n"
Expand Down
2 changes: 1 addition & 1 deletion src/routing/route.cr
Expand Up @@ -45,7 +45,7 @@ module Frost
io << " params[#{ name }] = URI.unescape($#{ index + 1}) if $#{ index + 1 }?\n"
end

io << " controller = #{ controller_class }.new(request, params, #{ action.inspect })\n"
io << " controller = #{ controller_class }.new(context, params, #{ action.inspect })\n"
io << " controller.run_action do\n"
io << " controller.#{ action }\n"
io << " controller.render unless controller.already_rendered?\n"
Expand Down
42 changes: 15 additions & 27 deletions src/server/handlers/deflate_handler.cr
@@ -1,36 +1,24 @@
require "http/server"
require "http/client/response"

class Zlib::Deflate
def close
input = @input
abstract class Frost::Server
class DeflateHandler < HTTP::DeflateHandler
DEFAULT_DEFLATE_TYPES = %w(text/html text/plain text/xml text/css text/javascript application/javascript application/json)

if input.responds_to?(:close)
input.close
end
end
end

class HTTP::Response
def body_io=(@body_io)
@body = nil
headers.delete("Content-length")
end
end
property :deflate_types

class HTTP::DeflateHandler < HTTP::Handler
def call(request)
response = call_next(request)
def initialize(@deflate_types = DEFAULT_DEFLATE_TYPES)
end

if should_deflate?(request, response)
io = if response.body?
MemoryIO.new(response.body)
else
response.body_io
end
response.body_io = Zlib::Deflate.new(io)
response.headers["Content-Encoding"] = "deflate"
def call(context)
super if deflate?(context.response)
end

response
def deflate?(response)
return false unless HTTP::Client::Response.mandatory_body?(response.status_code)
return false if response.headers["Cache-Control"]? =~ /\bno-transform\b/
return false unless content_type = response.headers["Content-Type"]?
deflate_types.includes?(content_type)
end
end
end
18 changes: 9 additions & 9 deletions src/server/handlers/https_everywhere_handler.cr
Expand Up @@ -10,24 +10,24 @@ require "http/server"
# The redirection will be a 308 Permanent Redirect HTTP response, so browsers
# are told to reissue the request permanently and to use the same HTTP method
# (eg: POST or DELETE) over HTTPS.
module Frost::Server
abstract class Frost::Server
class HttpsEverywhereHandler < HTTP::Handler
getter :status_code

def initialize(@status_code = 308)
end

def call(request)
if proto = request.headers["X-Forwarded-Proto"]?
return redirect_to_https(request) if proto != "https"
def call(ctx)
if proto = ctx.request.headers["X-Forwarded-Proto"]?
return redirect_to_https(ctx) if proto != "https"
end
call_next(request)
call_next(ctx)
end

def redirect_to_https(request)
response = HTTP::Response.new(status_code)
response.headers["Location"] = "https://#{ request.headers["Host"] }#{ request.resource }"
response
def redirect_to_https(ctx)
ctx.response.headers["Location"] =
"https://#{ ctx.request.headers["Host"] }#{ ctx.request.resource }"
nil
end
end
end
14 changes: 7 additions & 7 deletions src/server/handlers/log_handler.cr
@@ -1,13 +1,13 @@
require "http/server"

module Frost::Server
abstract class Frost::Server
class LogHandler < HTTP::Handler
def call(request)
time = Time.now
call_next(request).tap do |response|
elapsed = elapsed_text(Time.now - time)
Frost.logger.info "#{ request.method } #{ request.path.inspect } #{ response.status_code } (#{ elapsed })"
end
def call(ctx)
start = Time.now
call_next(ctx)
elapsed = elapsed_text(Time.now - start)
Frost.logger.info "#{ ctx.request.method } #{ ctx.request.path.inspect } #{ ctx.response.status_code } (#{ elapsed })"
nil
end

private def elapsed_text(elapsed)
Expand Down

0 comments on commit 8cfa856

Please sign in to comment.