├── .gitignore ├── Gemfile ├── Rakefile ├── rack-parser.gemspec ├── spec ├── spec_helper.rb └── parser_spec.rb ├── MIT-LICENSE ├── lib └── rack │ └── parser.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in rack-parser.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rake/testtask' 4 | Rake::TestTask.new(:spec) do |test| 5 | test.libs << 'lib' << 'spec' 6 | test.pattern = 'spec/**/*_spec.rb' 7 | test.warning = true 8 | test.verbose = true 9 | end 10 | -------------------------------------------------------------------------------- /rack-parser.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "rack-parser" 6 | s.version = "0.7.0" 7 | s.authors = ["Arthur Chiu"] 8 | s.email = ["mr.arthur.chiu@gmail.com"] 9 | s.homepage = "https://www.github.com/achiu/rack-parser" 10 | s.summary = %q{Rack Middleware for parsing post body data} 11 | s.description = %q{Rack Middleware for parsing post body data for json, xml and various content types} 12 | 13 | s.rubyforge_project = "rack-parser" 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | s.add_dependency 'rack' 21 | s.add_development_dependency 'minitest' 22 | s.add_development_dependency 'rack-test' 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/autorun' 3 | require 'rack' 4 | require 'rack/test' 5 | require 'rack/builder' 6 | require 'json' 7 | require File.expand_path('../../lib/rack/parser', __FILE__) 8 | 9 | class ParserApp 10 | def call(env) 11 | request = Rack::Request.new(env) 12 | type = { 'Content-Type' => 'text/plain' } 13 | code, body = 14 | case request.path 15 | when '/' then [200, 'Hello World'] 16 | when '/post' then [200, request.params.inspect] 17 | when '/error' then raise(StandardError, 'error!') 18 | else 19 | [404, 'Nothing'] 20 | end 21 | [code, type, body] 22 | end 23 | end 24 | 25 | class Minitest::Spec 26 | include Rack::Test::Methods 27 | 28 | def app(*middleware) 29 | @builder = Rack::Builder.new 30 | @builder.use(*@stack) 31 | @builder.run ParserApp.new 32 | @builder.to_app 33 | end 34 | 35 | def stack(*middleware) 36 | @stack = middleware 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Arthur Chiu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /lib/rack/parser.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Parser 3 | 4 | POST_BODY = 'rack.input'.freeze 5 | FORM_INPUT = 'rack.request.form_input'.freeze 6 | FORM_HASH = 'rack.request.form_hash'.freeze 7 | PARSER_RESULT = 'rack.parser.result'.freeze 8 | 9 | JSON_PARSER = proc { |data| JSON.parse data } 10 | ERROR_HANDLER = proc { |err, type| [400, {}, ['']] } 11 | 12 | attr_reader :parsers, :handlers, :logger 13 | 14 | def initialize(app, options = {}) 15 | @app = app 16 | @parsers = options[:parsers] || { %r{json} => JSON_PARSER } 17 | @handlers = options[:handlers] || {} 18 | @logger = options[:logger] 19 | end 20 | 21 | def call(env) 22 | type = Rack::Request.new(env).media_type 23 | parser = match_content_types_for(parsers, type) if type 24 | return @app.call(env) unless parser 25 | body = env[POST_BODY].read ; env[POST_BODY].rewind 26 | return @app.call(env) unless body && !body.empty? 27 | begin 28 | parsed = parser.last.call body 29 | env[PARSER_RESULT] = parsed 30 | env.update FORM_HASH => parsed, FORM_INPUT => env[POST_BODY] if parsed.is_a?(Hash) 31 | rescue StandardError => e 32 | warn! e, type 33 | handler = match_content_types_for handlers, type 34 | handler ||= ['default', ERROR_HANDLER] 35 | return handler.last.call(e, type) 36 | end 37 | @app.call env 38 | end 39 | 40 | # Private: send a warning out to the logger 41 | # 42 | # error - Exception object 43 | # type - String of the Content-Type 44 | # 45 | def warn!(error, type) 46 | return unless logger 47 | message = "[Rack::Parser] Error on %s : %s" % [type, error.to_s] 48 | logger.warn message 49 | end 50 | 51 | # Private: matches content types for the given media type 52 | # 53 | # content_types - An array of the parsers or handlers options 54 | # type - The media type. gathered from the Rack::Request 55 | # 56 | # Returns The match from the parser/handler hash or nil 57 | def match_content_types_for(content_types, type) 58 | content_types.detect do |content_type, _| 59 | content_type.is_a?(Regexp) ? type.match(content_type) : type == content_type 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rack::Parser # 2 | 3 | ### Looking for a new maintainer! ### 4 | 5 | Rack::Parser is a rack middleware that allows your application to do decode/parse incoming post data 6 | into param hashes for your applications to use. You can provide a custom 7 | Parser for things like JSON, XML, MSGPACK using your library of choice. 8 | 9 | ## Installation ## 10 | 11 | install it via rubygems: 12 | 13 | ``` 14 | gem install rack-parser 15 | ``` 16 | 17 | or put it in your Gemfile: 18 | 19 | ```ruby 20 | # Gemfile 21 | 22 | gem 'rack-parser', :require => 'rack/parser' 23 | ``` 24 | 25 | ## Usage ## 26 | 27 | In a Sinatra or [Padrino](http://padrinorb.com) application, it would probably be something like: 28 | 29 | ```ruby 30 | # app.rb 31 | 32 | use Rack::Parser, :parsers => { 'application/json' => proc { |data| JSON.parse data }, 33 | 'application/xml' => proc { |data| XML.parse data }, 34 | %r{msgpack} => proc { |data| Msgpack.parse data } 35 | } 36 | ``` 37 | 38 | ### Content Type Parsing ### 39 | 40 | By default, Rack::Parser uses `JSON` decode/parse your JSON Data. This can be overwritten if you choose not to use 41 | them. You can do it like so: 42 | 43 | ```ruby 44 | use Rack::Parser, :parsers => { 45 | 'application/json' => proc { |body| MyCustomJsonEngine.do_it body }, 46 | 'application/xml' => proc { |body| MyCustomXmlEngine.decode body }, 47 | 'application/roll' => proc { |body| 'never gonna give you up' } 48 | } 49 | ``` 50 | 51 | ### Error Handling ### 52 | 53 | Rack::Parser comes with a default error handling response that is sent 54 | if an error is to occur. If a `logger` is present, it will try to `warn` 55 | with the content type and error message. 56 | 57 | You can additionally customize the error handling response as well to 58 | whatever it is you like: 59 | 60 | ```ruby 61 | use Rack::Parser, :handlers => { 62 | 'application/json' => proc { |e, type| [400, { 'Content-Type' => type }, ["broke"]] } 63 | } 64 | ``` 65 | 66 | The error handler expects to pass both the `error` and `content_type` so 67 | that you can use them within your responses. In addition, you can 68 | override the default response as well. 69 | 70 | If no content_type error handling response is present, it will return `400` 71 | 72 | Do note, the error handler rescues exceptions that are descents of `StandardError`. See 73 | http://www.mikeperham.com/2012/03/03/the-perils-of-rescue-exception/ 74 | 75 | ### Regex Matching ### 76 | 77 | With version `0.4.0`, you can specify regex matches for the content 78 | types that you want the `parsers` and `handlers` to match. 79 | 80 | NOTE: you need to explicitly pass a `Regexp` for it to regex match. 81 | 82 | ```ruby 83 | parser = proc { |data| JSON.parse data } 84 | handler = proc { |e, type| [400, {}, 'boop'] } 85 | use Rack::Parser, :parsers => { %r{json} => parser }, 86 | :handlers => { %r{heyyyy} => handler } 87 | ``` 88 | 89 | ## Inspirations ## 90 | 91 | This project came to being because of: 92 | 93 | * [Niko Dittmann's](https://www.github.com/niko) [rack-post-body-to-params](https://www.github.com/niko/rack-post-body-to-params) which some of its ideas are instilled in this middleware. 94 | * Rack::PostBodyContentTypeParser from rack-contrib which proved to be an inspiration for both libraries. 95 | 96 | 97 | ## External Sources/Documentations 98 | 99 | * [Sinatra recipes](https://github.com/sinatra/sinatra-recipes/blob/master/middleware/rack_parser.md) - mini tutorial on using rack-parser (thanks to [Eric Gjertsen](https://github.com/ericgj)) 100 | 101 | 102 | ## Contributors ## 103 | 104 | * [Stephen Becker IV](https://github.com/sbeckeriv) - For initial custom error response handling work. 105 | * [Tom May](https://github.com/tommay) - skip loading post body unless content type is set. 106 | * [Moonsik Kang](https://github.com/deepblue) - skip rack parser for content types that are not explicitly set. 107 | * [Guillermo Iguaran](https://github.com/guilleiguaran) - Updating `multi_xml` version dependency for XML/YAML exploit 108 | * [Doug Orleans](https://github.com/dougo) - Handle only post-body parsing errors and let upstream errors propogate downstream 109 | * [Akshay Moghe](https://github.com/amoghe) - Make default error handler rack compliant by responding to #each and use StandardError 110 | 111 | ## Copyright 112 | 113 | Copyright © 2011,2012,2013,2014,2015,2016 Arthur Chiu. See [MIT-LICENSE](https://github.com/achiu/rack-parser/blob/master/MIT-LICENSE) for details. 114 | 115 | -------------------------------------------------------------------------------- /spec/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', __FILE__) 2 | 3 | describe Rack::Parser do 4 | 5 | it "allows you to setup parsers for content types" do 6 | middleware = Rack::Parser.new ParserApp, :parsers => { 'foo' => 'bar' } 7 | assert_equal 'bar', middleware.parsers['foo'] 8 | end 9 | 10 | it "should not remove fields from options in setup" do 11 | options = {:parsers => { 'foo' => 'bar' }} 12 | middleware = Rack::Parser.new ParserApp, options 13 | refute_nil options[:parsers] 14 | end 15 | 16 | it "allows you to setup error handlers" do 17 | stack = Rack::Parser.new ParserApp, :handlers => { 'foo' => 'bar' } 18 | assert_equal 'bar', stack.handlers['foo'] 19 | end 20 | 21 | it "parses a Content-Type" do 22 | payload = JSON.dump(:a => 1) 23 | parser = proc { |data| JSON.parse data } 24 | stack Rack::Parser, :parsers => { 'application/json' => parser } 25 | post '/post', payload, { 'CONTENT_TYPE' => 'application/json' } 26 | 27 | assert last_response.ok? 28 | assert_equal "{\"a\"=>1}", last_response.body 29 | end 30 | 31 | it "does nothing if unmatched Content-Type" do 32 | payload = JSON.dump(:a => 1) 33 | parser = proc { |data| JSON.parse data } 34 | stack Rack::Parser, :parsers => { 'application/json' => parser } 35 | post '/post', payload, { 'CONTENT_TYPE' => 'application/xml' } 36 | 37 | assert last_response.ok? 38 | assert_equal "{}", last_response.body # request.params won't pick up this content type 39 | end 40 | 41 | it "matches Content-Type by regex" do 42 | payload = JSON.dump(:a => 2) 43 | parser = proc { |data| JSON.parse data } 44 | stack Rack::Parser, :parsers => { %r{json} => parser } 45 | post '/post', payload, { 'CONTENT_TYPE' => 'application/vnd.foo+json' } 46 | 47 | assert last_response.ok? 48 | assert_equal "{\"a\"=>2}", last_response.body 49 | end 50 | 51 | it 'matches ambiguous string Content-Type and forces explicit regex' do 52 | payload = JSON.dump(:a => 2) 53 | parser = proc { |data| JSON.parse data } 54 | stack Rack::Parser, :parsers => { 'application/vnd.foo+json' => parser } 55 | post '/post', payload, { 'CONTENT_TYPE' => 'application/vnd.foo+json' } 56 | 57 | assert last_response.ok? 58 | assert_equal "{\"a\"=>2}", last_response.body 59 | end 60 | 61 | it "handles upstream errors" do 62 | assert_raises StandardError, 'error!' do 63 | parser = proc { |data| JSON.parse data } 64 | stack Rack::Parser, :parsers => { %r{json} => parser } 65 | post '/error', '{}', { 'CONTENT_TYPE' => 'application/json' } 66 | end 67 | end 68 | 69 | it "returns a default error" do 70 | parser = proc { |data| raise StandardError, 'wah wah' } 71 | stack Rack::Parser, :parsers => { %r{json} => parser } 72 | post '/post', '{}', { 'CONTENT_TYPE' => 'application/vnd.foo+json' } 73 | 74 | assert_equal 400, last_response.status 75 | end 76 | 77 | it "returns a custom error message" do 78 | parser = proc { |data| raise StandardError, "wah wah" } 79 | handler = proc { |err, type| [500, {}, "%s : %s" % [type, err]] } 80 | stack Rack::Parser, :parsers => { %r{json} => parser }, 81 | :handlers => { %r{json} => handler } 82 | post '/post', '{}', { 'CONTENT_TYPE' => 'application/vnd.foo+json' } 83 | 84 | assert_equal 500, last_response.status 85 | assert_equal 'application/vnd.foo+json : wah wah', last_response.body 86 | end 87 | 88 | it 'returns a custome error for ambiguous string Content-Type and forces explicit regex' do 89 | parser = proc { |data| raise StandardError, "wah wah" } 90 | handler = proc { |err, type| [500, {}, "%s : %s" % [type, err]] } 91 | stack Rack::Parser, :parsers => { %r{json} => parser }, 92 | :handlers => { 'application/vnd.foo+json' => handler } 93 | post '/post', '{}', { 'CONTENT_TYPE' => 'application/vnd.foo+json' } 94 | 95 | assert_equal 500, last_response.status 96 | assert_equal 'application/vnd.foo+json : wah wah', last_response.body 97 | end 98 | 99 | it "parses an array but do not set it to params" do 100 | payload = JSON.dump([1,2,3]) 101 | parser = proc { |data| JSON.parse data } 102 | stack Rack::Parser, :parsers => { 'application/json' => parser } 103 | post '/post', payload, { 'CONTENT_TYPE' => 'application/json' } 104 | 105 | assert last_response.ok? 106 | assert_equal last_request.env['rack.parser.result'], [1, 2, 3] 107 | assert_equal last_request.env['rack.request.form_hash'], nil 108 | end 109 | end 110 | --------------------------------------------------------------------------------