diff --git a/CHANGELOG.md b/CHANGELOG.md index c1062d7..f041528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,34 @@ # Next -- `env.params.files` is now an `Array(FileUpload)`. You can iterate over to access the images. +- *[breaking change]* Removed `env.params.files`. You can use Crystal's built-in `HTTP::FormData.parse` instead ```ruby -env.params.files.each do |file| +post "/upload" do |env| + HTTP::FormData.parse(env.request) do |upload| + filename = file.filename - filename = file.filename - - if !filename.is_a?(String) - "No filename included in upload" - else - file_path = ::File.join [Kemal.config.public_folder, "uploads/", filename] - File.open(file_path, "w") do |f| - IO.copy(file.tmpfile, f) + if !filename.is_a?(String) + "No filename included in upload" + else + file_path = ::File.join [Kemal.config.public_folder, "uploads/", filename] + File.open(file_path, "w") do |f| + IO.copy(file.tmpfile, f) + end + "Upload OK" end - "Upload OK" end ``` +- *[breaking change]* From now on to access dynamic url params in a WebSocket route you have to use + +```ruby +ws "/:id" do |socket, context| + id = context.ws_route_lookup.params["id"] +end +``` + +- *[breaking change]* Removed `_method` magic param + # 0.23.0 (17-06-2018) - Crystal 0.25.0 support 🎉 diff --git a/spec/middleware/filters_spec.cr b/spec/middleware/filters_spec.cr index 41853be..9bc2564 100644 --- a/spec/middleware/filters_spec.cr +++ b/spec/middleware/filters_spec.cr @@ -13,8 +13,8 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end @@ -33,14 +33,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end @@ -61,14 +61,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") end @@ -85,8 +85,8 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end @@ -105,14 +105,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end @@ -132,14 +132,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") end @@ -166,20 +166,20 @@ describe "Kemal::FilterHandler" do test_filter_second.modified.should eq("false") test_filter_third.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") request = HTTP::Request.new("PUT", "/greetings") - create_request_and_return_io(filter_middleware, request) - io_with_context = create_request_and_return_io(kemal, request) + create_request_and_return_io_and_context(filter_middleware, request) + io_with_context = create_request_and_return_io_and_context(kemal, request)[0] client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end diff --git a/spec/param_parser_spec.cr b/spec/param_parser_spec.cr index 305a0a3..d63a229 100644 --- a/spec/param_parser_spec.cr +++ b/spec/param_parser_spec.cr @@ -28,8 +28,8 @@ describe "ParamParser" do end request = HTTP::Request.new("POST", "/hello/cemal") # Radix tree MUST be run to parse url params. - create_request_and_return_io(kemal, request) - url_params = Kemal::ParamParser.new(request).url + context = create_request_and_return_io_and_context(kemal, request)[1] + url_params = Kemal::ParamParser.new(request, context.route_lookup.params).url url_params["hasan"].should eq "cemal" end @@ -43,8 +43,8 @@ describe "ParamParser" do end request = HTTP::Request.new("POST", "/hello/sam%2Bspec%40gmail.com/%2419.99/a%C3%B1o") # Radix tree MUST be run to parse url params. - create_request_and_return_io(kemal, request) - url_params = Kemal::ParamParser.new(request).url + context = create_request_and_return_io_and_context(kemal, request)[1] + url_params = Kemal::ParamParser.new(request, context.route_lookup.params).url url_params["email"].should eq "sam+spec@gmail.com" url_params["money"].should eq "$19.99" url_params["spanish"].should eq "año" diff --git a/spec/route_handler_spec.cr b/spec/route_handler_spec.cr index 5705618..a6db5e1 100644 --- a/spec/route_handler_spec.cr +++ b/spec/route_handler_spec.cr @@ -102,48 +102,6 @@ describe "Kemal::RouteHandler" do client_response.body.should eq("Skills ruby,crystal") end - it "checks for _method param in POST request to simulate PUT" do - put "/" do - "Hello World from PUT" - end - request = HTTP::Request.new( - "POST", - "/", - body: "_method=PUT", - headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded"} - ) - client_response = call_request_on_app(request) - client_response.body.should eq("Hello World from PUT") - end - - it "checks for _method param in POST request to simulate PATCH" do - patch "/" do - "Hello World from PATCH" - end - request = HTTP::Request.new( - "POST", - "/", - body: "_method=PATCH", - headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"} - ) - client_response = call_request_on_app(request) - client_response.body.should eq("Hello World from PATCH") - end - - it "checks for _method param in POST request to simulate DELETE" do - delete "/" do - "Hello World from DELETE" - end - request = HTTP::Request.new( - "POST", - "/", - body: "_method=DELETE", - headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"} - ) - client_response = call_request_on_app(request) - client_response.body.should eq("Hello World from DELETE") - end - it "can process HTTP HEAD requests for defined GET routes" do get "/" do "Hello World from GET" diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index a4876c2..4d27003 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -29,17 +29,17 @@ end add_context_storage_type(TestContextStorageType) add_context_storage_type(AnotherContextStorageType) -def create_request_and_return_io(handler, request) +def create_request_and_return_io_and_context(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler.call(context) response.close io.rewind - io + {io, context} end -def create_ws_request_and_return_io(handler, request) +def create_ws_request_and_return_io_and_context(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) @@ -49,6 +49,7 @@ def create_ws_request_and_return_io(handler, request) # Raises because the IO::Memory is empty end io.rewind + {io, context} end def call_request_on_app(request) diff --git a/spec/static_file_handler_spec.cr b/spec/static_file_handler_spec.cr index 6da77a3..1aac161 100644 --- a/spec/static_file_handler_spec.cr +++ b/spec/static_file_handler_spec.cr @@ -30,6 +30,7 @@ describe Kemal::StaticFileHandler do headers = HTTP::Headers{"If-None-Match" => etag} response = handle HTTP::Request.new("GET", "/dir/test.txt", headers) + response.headers["Content-Type"]?.should be_nil response.status_code.should eq(304) response.body.should eq "" end diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index bb867b2..bc02d3c 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -32,13 +32,13 @@ describe "Kemal::WebSocketHandler" do } request = HTTP::Request.new("GET", "/", headers) - io_with_context = create_ws_request_and_return_io(handler, request) + io_with_context = create_ws_request_and_return_io_and_context(handler, request)[0] io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") end it "fetches named url parameters" do handler = Kemal::WebSocketHandler::INSTANCE - ws "/:id" { |_, c| c.params.url["id"] } + ws "/:id" { |_, c| c.ws_route_lookup.params["id"] } headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", @@ -46,7 +46,7 @@ describe "Kemal::WebSocketHandler" do "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/1234", headers) - io_with_context = create_ws_request_and_return_io(handler, request) + io_with_context = create_ws_request_and_return_io_and_context(handler, request)[0] io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end diff --git a/src/kemal/ext/context.cr b/src/kemal/ext/context.cr index bd8c208..9f2d3c0 100644 --- a/src/kemal/ext/context.cr +++ b/src/kemal/ext/context.cr @@ -14,12 +14,7 @@ class HTTP::Server end def params - @request.url_params ||= route_lookup.params - @params ||= if @request.param_parser - @request.param_parser.not_nil! - else - Kemal::ParamParser.new(@request) - end + @params ||= Kemal::ParamParser.new(@request, route_lookup.params) end def redirect(url : String, status_code : Int32 = 302) @@ -36,7 +31,7 @@ class HTTP::Server end def route_lookup - Kemal::RouteHandler::INSTANCE.lookup_route(@request.override_method.as(String), @request.path) + Kemal::RouteHandler::INSTANCE.lookup_route(@request.method.as(String), @request.path) end def route_defined? diff --git a/src/kemal/ext/request.cr b/src/kemal/ext/request.cr deleted file mode 100644 index bc0569b..0000000 --- a/src/kemal/ext/request.cr +++ /dev/null @@ -1,32 +0,0 @@ -class HTTP::Request - property override_method - property url_params : Hash(String, String)? - getter param_parser : Kemal::ParamParser? - - def override_method - @override_method ||= check_for_method_override! - end - - def content_type - @headers["Content-Type"]? - end - - # Checks if method contained in _method param is valid one - def self.override_method_valid?(override_method : String) - override_method = override_method.upcase - override_method == "PUT" || override_method == "PATCH" || override_method == "DELETE" - end - - # Checks if request params contain _method param to override request incoming method - private def check_for_method_override! - @override_method = @method - if @method == "POST" - @param_parser = Kemal::ParamParser.new(self) - params = @param_parser.not_nil!.body - if params.has_key?("_method") && HTTP::Request.override_method_valid?(params["_method"]) - @override_method = params["_method"] - end - end - @override_method - end -end diff --git a/src/kemal/file_upload.cr b/src/kemal/file_upload.cr deleted file mode 100644 index 562c18b..0000000 --- a/src/kemal/file_upload.cr +++ /dev/null @@ -1,25 +0,0 @@ -module Kemal - # :nodoc: - struct FileUpload - getter tmpfile : Tempfile - getter filename : String? - getter headers : HTTP::Headers - getter creation_time : Time? - getter modification_time : Time? - getter read_time : Time? - getter size : UInt64? - - def initialize(upload) - @tmpfile = Tempfile.new(filename) - ::File.open(@tmpfile.path, "w") do |file| - IO.copy(upload.body, file) - end - @filename = upload.filename - @headers = upload.headers - @creation_time = upload.creation_time - @modification_time = upload.modification_time - @read_time = upload.read_time - @size = upload.size - end - end -end diff --git a/src/kemal/filter_handler.cr b/src/kemal/filter_handler.cr index 1d842f2..716e25e 100644 --- a/src/kemal/filter_handler.cr +++ b/src/kemal/filter_handler.cr @@ -14,12 +14,12 @@ module Kemal def call(context : HTTP::Server::Context) return call_next(context) unless context.route_defined? call_block_for_path_type("ALL", context.request.path, :before, context) - call_block_for_path_type(context.request.override_method, context.request.path, :before, context) + call_block_for_path_type(context.request.method, context.request.path, :before, context) if Kemal.config.error_handlers.has_key?(context.response.status_code) raise Kemal::Exceptions::CustomException.new(context) end call_next(context) - call_block_for_path_type(context.request.override_method, context.request.path, :after, context) + call_block_for_path_type(context.request.method, context.request.path, :after, context) call_block_for_path_type("ALL", context.request.path, :after, context) context end diff --git a/src/kemal/helpers/exceptions.cr b/src/kemal/helpers/exceptions.cr index 327a9d1..53520b3 100644 --- a/src/kemal/helpers/exceptions.cr +++ b/src/kemal/helpers/exceptions.cr @@ -8,7 +8,7 @@ module Kemal::Exceptions class RouteNotFound < Exception def initialize(context : HTTP::Server::Context) - super "Requested path: '#{context.request.override_method}:#{context.request.path}' was not found." + super "Requested path: '#{context.request.method}:#{context.request.path}' was not found." end end diff --git a/src/kemal/param_parser.cr b/src/kemal/param_parser.cr index 65834e4..66626b4 100644 --- a/src/kemal/param_parser.cr +++ b/src/kemal/param_parser.cr @@ -9,14 +9,11 @@ module Kemal PARTS = %w(url query body json) # :nodoc: alias AllParamTypes = Nil | String | Int64 | Float64 | Bool | Hash(String, JSON::Any) | Array(JSON::Any) - getter files - def initialize(@request : HTTP::Request) - @url = {} of String => String + def initialize(@request : HTTP::Request, @url : Hash(String, String) = {} of String => String) @query = HTTP::Params.new({} of String => Array(String)) @body = HTTP::Params.new({} of String => Array(String)) @json = {} of String => AllParamTypes - @files = [] of FileUpload @url_parsed = false @query_parsed = false @body_parsed = false @@ -42,16 +39,12 @@ module Kemal {% end %} private def parse_body - content_type = @request.content_type + content_type = @request.headers["Content-Type"]? return unless content_type if content_type.try(&.starts_with?(URL_ENCODED_FORM)) @body = parse_part(@request.body) return end - if content_type.try(&.starts_with?(MULTIPART_FORM)) - parse_file_upload - return - end end private def parse_query @@ -59,23 +52,7 @@ module Kemal end private def parse_url - if params = @request.url_params - params.each do |key, value| - @url[key] = unescape_url_param(value) - end - end - end - - private def parse_file_upload - HTTP::FormData.parse(@request) do |upload| - next unless upload - filename = upload.filename - if !filename.nil? - @files << FileUpload.new(upload: upload) - else - @body.add(upload.name, upload.body.gets_to_end) - end - end + @url.each { |key, value| @url[key] = unescape_url_param(value) } end # Parses JSON request body if Content-Type is `application/json`. diff --git a/src/kemal/static_file_handler.cr b/src/kemal/static_file_handler.cr index fc9e036..8fa802e 100644 --- a/src/kemal/static_file_handler.cr +++ b/src/kemal/static_file_handler.cr @@ -50,21 +50,17 @@ module Kemal return call_next(context) end elsif File.exists?(file_path) - return if etag(context, file_path) + last_modified = modification_time(file_path) + add_cache_headers(context.response.headers, last_modified) + + if cache_request?(context, last_modified) + context.response.status_code = 304 + return + end send_file(context, file_path) else call_next(context) end end - - private def etag(context : HTTP::Server::Context, file_path : String) - etag = %{W/"#{File.info(file_path).modification_time.epoch.to_s}"} - context.response.headers["ETag"] = etag - return false if !context.request.headers["If-None-Match"]? || context.request.headers["If-None-Match"] != etag - context.response.headers.delete "Content-Type" - context.response.content_length = 0 - context.response.status_code = 304 # not modified - return true - end end end diff --git a/src/kemal/websocket_handler.cr b/src/kemal/websocket_handler.cr index dd31bf8..9550bef 100644 --- a/src/kemal/websocket_handler.cr +++ b/src/kemal/websocket_handler.cr @@ -11,7 +11,6 @@ module Kemal def call(context : HTTP::Server::Context) return call_next(context) unless context.ws_route_defined? && websocket_upgrade_request?(context) - context.request.url_params ||= context.ws_route_lookup.params content = context.websocket.call(context) context.response.print(content) context