├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── CONTRIBUTING.md ├── Changelog.md ├── Gemfile ├── Guardfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin └── httparty ├── cucumber.yml ├── docs └── README.md ├── examples ├── README.md ├── aaws.rb ├── basic.rb ├── body_stream.rb ├── crack.rb ├── custom_parsers.rb ├── delicious.rb ├── google.rb ├── headers_and_user_agents.rb ├── idn.rb ├── logging.rb ├── microsoft_graph.rb ├── multipart.rb ├── nokogiri_html_parser.rb ├── party_foul_mode.rb ├── peer_cert.rb ├── rescue_json.rb ├── rubyurl.rb ├── stackexchange.rb ├── stream_download.rb ├── tripit_sign_in.rb ├── twitter.rb └── whoismyrep.rb ├── features ├── basic_authentication.feature ├── command_line.feature ├── deals_with_http_error_codes.feature ├── digest_authentication.feature ├── handles_compressed_responses.feature ├── handles_multiple_formats.feature ├── steps │ ├── env.rb │ ├── httparty_response_steps.rb │ ├── httparty_steps.rb │ ├── mongrel_helper.rb │ └── remote_service_steps.rb ├── supports_marshalling_with_logger_and_proc.feature ├── supports_read_timeout_option.feature ├── supports_redirection.feature └── supports_timeout_option.feature ├── httparty.gemspec ├── lib ├── httparty.rb └── httparty │ ├── connection_adapter.rb │ ├── cookie_hash.rb │ ├── decompressor.rb │ ├── exceptions.rb │ ├── hash_conversions.rb │ ├── headers_processor.rb │ ├── logger │ ├── apache_formatter.rb │ ├── curl_formatter.rb │ ├── logger.rb │ └── logstash_formatter.rb │ ├── module_inheritable_attributes.rb │ ├── net_digest_auth.rb │ ├── parser.rb │ ├── request.rb │ ├── request │ ├── body.rb │ └── multipart_boundary.rb │ ├── response.rb │ ├── response │ └── headers.rb │ ├── response_fragment.rb │ ├── text_encoder.rb │ ├── utils.rb │ └── version.rb ├── script └── release ├── spec ├── fixtures │ ├── delicious.xml │ ├── empty.xml │ ├── example.html │ ├── ssl │ │ ├── generate.sh │ │ ├── generated │ │ │ ├── bogushost.crt │ │ │ ├── ca.crt │ │ │ ├── ca.key │ │ │ ├── selfsigned.crt │ │ │ ├── server.crt │ │ │ └── server.key │ │ └── openssl-exts.cnf │ ├── tiny.gif │ ├── twitter.csv │ ├── twitter.json │ ├── twitter.xml │ └── undefined_method_add_node_for_nil.xml ├── httparty │ ├── connection_adapter_spec.rb │ ├── cookie_hash_spec.rb │ ├── decompressor_spec.rb │ ├── exception_spec.rb │ ├── hash_conversions_spec.rb │ ├── headers_processor_spec.rb │ ├── logger │ │ ├── apache_formatter_spec.rb │ │ ├── curl_formatter_spec.rb │ │ ├── logger_spec.rb │ │ └── logstash_formatter_spec.rb │ ├── net_digest_auth_spec.rb │ ├── parser_spec.rb │ ├── request │ │ └── body_spec.rb │ ├── request_spec.rb │ ├── response_fragment_spec.rb │ ├── response_spec.rb │ └── ssl_spec.rb ├── httparty_spec.rb ├── spec_helper.rb └── support │ ├── ssl_test_helper.rb │ ├── ssl_test_server.rb │ └── stub_response.rb └── website ├── css └── common.css └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://EditorConfig.org 3 | 4 | root = true 5 | [*] 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | 9 | [**.rb] 10 | indent_size = 2 11 | indent_style = spaces 12 | insert_final_newline = true 13 | 14 | [**.xml] 15 | trim_trailing_whitespace = false 16 | 17 | [**.html] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | ruby: 9 | - 2.7 10 | - "3.0" # Quoted, to avoid YAML float 3.0 interplated to "3" 11 | - 3.1 12 | - 3.2 13 | - 3.3 14 | - 3.4 15 | steps: 16 | - name: Check out repository code 17 | uses: actions/checkout@v4 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler-cache: true # Run "bundle install", and cache the result automatically. 23 | - name: Run Rake 24 | run: bundle exec rake 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .DS_Store 3 | .yardoc/ 4 | doc/ 5 | tmp/ 6 | log/ 7 | pkg/ 8 | *.swp 9 | /.bundle 10 | .rvmrc 11 | coverage 12 | *.gem 13 | .idea 14 | .tool-versions 15 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | # Offense count: 963 4 | # Cop supports --auto-correct. 5 | # Configuration parameters: EnforcedStyle, SupportedStyles. 6 | Style/StringLiterals: 7 | Enabled: false 8 | 9 | # Offense count: 327 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles. 12 | Style/SpaceInsideHashLiteralBraces: 13 | Enabled: false 14 | 15 | # Offense count: 33 16 | # Cop supports --auto-correct. 17 | # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. 18 | Style/SpaceInsideBlockBraces: 19 | Enabled: false 20 | 21 | # Offense count: 1 22 | # Cop supports --auto-correct. 23 | Style/SpaceBeforeSemicolon: 24 | Enabled: false 25 | 26 | # Offense count: 20 27 | # Cop supports --auto-correct. 28 | # Configuration parameters: EnforcedStyle, SupportedStyles. 29 | Style/SignalException: 30 | Enabled: false 31 | 32 | # Offense count: 1 33 | # Configuration parameters: Methods. 34 | Style/SingleLineBlockParams: 35 | Enabled: false 36 | 37 | # Offense count: 6 38 | # Cop supports --auto-correct. 39 | Style/PerlBackrefs: 40 | Enabled: false 41 | 42 | # Offense count: 2 43 | # Cop supports --auto-correct. 44 | # Configuration parameters: AllowAsExpressionSeparator. 45 | Style/Semicolon: 46 | Enabled: false 47 | 48 | # Offense count: 77 49 | # Cop supports --auto-correct. 50 | # Configuration parameters: EnforcedStyle, SupportedStyles. 51 | Style/BracesAroundHashParameters: 52 | Enabled: false 53 | 54 | # Offense count: 36 55 | Style/Documentation: 56 | Enabled: false 57 | 58 | # Offense count: 6 59 | # Cop supports --auto-correct. 60 | # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. 61 | Style/RegexpLiteral: 62 | Enabled: false 63 | 64 | # Offense count: 5 65 | # Cop supports --auto-correct. 66 | Style/NumericLiterals: 67 | MinDigits: 6 68 | 69 | # Offense count: 4 70 | # Cop supports --auto-correct. 71 | Lint/UnusedMethodArgument: 72 | Enabled: false 73 | 74 | # Offense count: 11 75 | # Cop supports --auto-correct. 76 | Lint/UnusedBlockArgument: 77 | Enabled: false 78 | 79 | # Offense count: 1 80 | Lint/Void: 81 | Enabled: false 82 | 83 | # Offense count: 22 84 | # Cop supports --auto-correct. 85 | # Configuration parameters: EnforcedStyle, SupportedStyles. 86 | Style/IndentHash: 87 | Enabled: false 88 | 89 | # Offense count: 7 90 | # Configuration parameters: MinBodyLength. 91 | Style/GuardClause: 92 | Enabled: false 93 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by `rubocop --auto-gen-config` 2 | # on 2015-04-24 07:22:28 +0200 using RuboCop version 0.30.0. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the offenses are removed from the code base. 5 | # Note that changes in the inspected code, or installation of new 6 | # versions of RuboCop, may require this file to be generated again. 7 | 8 | # Offense count: 33 9 | Lint/AmbiguousRegexpLiteral: 10 | Enabled: false 11 | 12 | # Offense count: 1 13 | # Configuration parameters: AlignWith, SupportedStyles. 14 | Lint/EndAlignment: 15 | Enabled: false 16 | 17 | # Offense count: 1 18 | Lint/SuppressedException: 19 | Enabled: false 20 | 21 | # Offense count: 5 22 | Lint/UselessAssignment: 23 | Enabled: false 24 | 25 | # Offense count: 23 26 | Metrics/AbcSize: 27 | Max: 86 28 | 29 | # Offense count: 1 30 | # Configuration parameters: CountComments. 31 | Metrics/ClassLength: 32 | Max: 285 33 | 34 | # Offense count: 8 35 | Metrics/CyclomaticComplexity: 36 | Max: 17 37 | 38 | # Offense count: 332 39 | # Configuration parameters: AllowURI, URISchemes. 40 | Metrics/LineLength: 41 | Max: 266 42 | 43 | # Offense count: 17 44 | # Configuration parameters: CountComments. 45 | Metrics/MethodLength: 46 | Max: 39 47 | 48 | # Offense count: 8 49 | Metrics/PerceivedComplexity: 50 | Max: 20 51 | 52 | # Offense count: 1 53 | Style/AccessorMethodName: 54 | Enabled: false 55 | 56 | # Offense count: 1 57 | Style/AsciiComments: 58 | Enabled: false 59 | 60 | # Offense count: 14 61 | # Cop supports --auto-correct. 62 | # Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. 63 | Style/BlockDelimiters: 64 | Enabled: false 65 | 66 | # Offense count: 2 67 | Style/CaseEquality: 68 | Enabled: false 69 | 70 | # Offense count: 3 71 | # Configuration parameters: IndentWhenRelativeTo, SupportedStyles, IndentOneStep. 72 | Style/CaseIndentation: 73 | Enabled: false 74 | 75 | # Offense count: 4 76 | # Configuration parameters: EnforcedStyle, SupportedStyles. 77 | Style/ClassAndModuleChildren: 78 | Enabled: false 79 | 80 | # Offense count: 7 81 | Style/ConstantName: 82 | Enabled: false 83 | 84 | # Offense count: 2 85 | Style/EachWithObject: 86 | Enabled: false 87 | 88 | # Offense count: 2 89 | # Cop supports --auto-correct. 90 | Style/ElseAlignment: 91 | Enabled: false 92 | 93 | # Offense count: 3 94 | # Cop supports --auto-correct. 95 | # Configuration parameters: EnforcedStyle, SupportedStyles. 96 | Style/FirstParameterIndentation: 97 | Enabled: false 98 | 99 | # Offense count: 2 100 | # Cop supports --auto-correct. 101 | # Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues. 102 | Style/HashSyntax: 103 | Enabled: false 104 | 105 | # Offense count: 7 106 | # Cop supports --auto-correct. 107 | # Configuration parameters: MaxLineLength. 108 | Style/IfUnlessModifier: 109 | Enabled: false 110 | 111 | # Offense count: 11 112 | # Cop supports --auto-correct. 113 | Style/Lambda: 114 | Enabled: false 115 | 116 | # Offense count: 1 117 | # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. 118 | Style/Next: 119 | Enabled: false 120 | 121 | # Offense count: 2 122 | # Configuration parameters: EnforcedStyle, SupportedStyles. 123 | Style/RaiseArgs: 124 | Enabled: false 125 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * Contributions will not be accepted without tests. 4 | * Please post unconfirmed bugs to the mailing list first: https://groups.google.com/forum/#!forum/httparty-gem 5 | * Don't change the version. The maintainers will handle that when they release. 6 | * Always provide as much information and reproducibility as possible when filing an issue or submitting a pull request. 7 | 8 | ## Workflow 9 | 10 | * Fork the project. 11 | * Run `bundle` 12 | * Run `bundle exec rake` 13 | * Make your feature addition or bug fix. 14 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 15 | * Run `bundle exec rake` (No, REALLY :)) 16 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull) 17 | * Send me a pull request. Bonus points for topic branches. 18 | 19 | ## Help and Docs 20 | 21 | * https://groups.google.com/forum/#!forum/httparty-gem 22 | * http://stackoverflow.com/questions/tagged/httparty 23 | * http://rdoc.info/projects/jnunemaker/httparty 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'base64' 5 | gem 'rake' 6 | gem 'mongrel', '1.2.0.pre2' 7 | gem 'json' 8 | 9 | group :development do 10 | gem 'guard' 11 | gem 'guard-rspec' 12 | gem 'guard-bundler' 13 | end 14 | 15 | group :test do 16 | gem 'rexml' 17 | gem 'rspec', '~> 3.4' 18 | gem 'simplecov', require: false 19 | gem 'aruba' 20 | gem 'cucumber', '~> 2.3' 21 | gem 'webmock' 22 | gem 'addressable' 23 | end 24 | 25 | group :development, :test do 26 | gem 'pry' 27 | end 28 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | rspec_options = { 2 | all_after_pass: false, 3 | all_on_start: false, 4 | failed_mode: :keep, 5 | cmd: 'bundle exec rspec', 6 | } 7 | 8 | guard 'rspec', rspec_options do 9 | watch(%r{^spec/.+_spec\.rb$}) 10 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 11 | watch('spec/spec_helper.rb') { "spec" } 12 | end 13 | 14 | guard 'bundler' do 15 | watch('Gemfile') 16 | watch(/^.+\.gemspec/) 17 | end 18 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 John Nunemaker 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httparty 2 | 3 | [![CI](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml/badge.svg)](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml) 4 | 5 | Makes http fun again! Ain't no party like a httparty, because a httparty don't stop. 6 | 7 | ## Install 8 | 9 | ``` 10 | gem install httparty 11 | ``` 12 | 13 | ## Requirements 14 | 15 | - Ruby 2.7.0 or higher 16 | - You like to party! 17 | 18 | ## Examples 19 | 20 | ```ruby 21 | # Use the class methods to get down to business quickly 22 | response = HTTParty.get('http://api.stackexchange.com/2.2/questions?site=stackoverflow') 23 | 24 | puts response.body, response.code, response.message, response.headers.inspect 25 | 26 | # Or wrap things up in your own class 27 | class StackExchange 28 | include HTTParty 29 | base_uri 'api.stackexchange.com' 30 | 31 | def initialize(service, page) 32 | @options = { query: { site: service, page: page } } 33 | end 34 | 35 | def questions 36 | self.class.get("/2.2/questions", @options) 37 | end 38 | 39 | def users 40 | self.class.get("/2.2/users", @options) 41 | end 42 | end 43 | 44 | stack_exchange = StackExchange.new("stackoverflow", 1) 45 | puts stack_exchange.questions 46 | puts stack_exchange.users 47 | ``` 48 | 49 | See the [examples directory](http://github.com/jnunemaker/httparty/tree/main/examples) for even more goodies. 50 | 51 | ## Command Line Interface 52 | 53 | httparty also includes the executable `httparty` which can be 54 | used to query web services and examine the resulting output. By default 55 | it will output the response as a pretty-printed Ruby object (useful for 56 | grokking the structure of output). This can also be overridden to output 57 | formatted XML or JSON. Execute `httparty --help` for all the 58 | options. Below is an example of how easy it is. 59 | 60 | ``` 61 | httparty "https://api.stackexchange.com/2.2/questions?site=stackoverflow" 62 | ``` 63 | 64 | ## Help and Docs 65 | 66 | - [Docs](https://github.com/jnunemaker/httparty/tree/main/docs) 67 | - https://github.com/jnunemaker/httparty/discussions 68 | - https://www.rubydoc.info/github/jnunemaker/httparty 69 | 70 | ## Contributing 71 | 72 | - Fork the project. 73 | - Run `bundle` 74 | - Run `bundle exec rake` 75 | - Make your feature addition or bug fix. 76 | - Add tests for it. This is important so I don't break it in a future version unintentionally. 77 | - Run `bundle exec rake` (No, REALLY :)) 78 | - Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull) 79 | - Send me a pull request. Bonus points for topic branches. 80 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rspec/core/rake_task' 3 | RSpec::Core::RakeTask.new(:spec) 4 | rescue LoadError 5 | end 6 | 7 | require 'cucumber/rake/task' 8 | Cucumber::Rake::Task.new(:features) 9 | 10 | task default: [:spec, :features] 11 | -------------------------------------------------------------------------------- /bin/httparty: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "optparse" 4 | require "pp" 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "/../lib")) 7 | require "httparty" 8 | 9 | opts = { 10 | action: :get, 11 | headers: {}, 12 | verbose: false 13 | } 14 | 15 | OptionParser.new do |o| 16 | o.banner = "USAGE: #{$PROGRAM_NAME} [options] [url]" 17 | 18 | o.on("-f", 19 | "--format [FORMAT]", 20 | "Output format to use instead of pretty-print ruby: " \ 21 | "plain, csv, json or xml") do |f| 22 | opts[:output_format] = f.downcase.to_sym 23 | end 24 | 25 | o.on("-a", 26 | "--action [ACTION]", 27 | "HTTP action: get (default), post, put, delete, head, or options") do |a| 28 | opts[:action] = a.downcase.to_sym 29 | end 30 | 31 | o.on("-d", 32 | "--data [BODY]", 33 | "Data to put in request body (prefix with '@' for file)") do |d| 34 | if d =~ /^@/ 35 | opts[:body] = open(d[1..-1]).read 36 | else 37 | opts[:body] = d 38 | end 39 | end 40 | 41 | o.on("-H", "--header [NAME:VALUE]", "Additional HTTP headers in NAME:VALUE form") do |h| 42 | abort "Invalid header specification, should be Name:Value" unless h =~ /.+:.+/ 43 | name, value = h.split(':') 44 | opts[:headers][name.strip] = value.strip 45 | end 46 | 47 | o.on("-v", "--verbose", "If set, print verbose output") do |v| 48 | opts[:verbose] = true 49 | end 50 | 51 | o.on("-u", "--user [CREDS]", "Use basic authentication. Value should be user:password") do |u| 52 | abort "Invalid credentials format. Must be user:password" unless u =~ /.*:.+/ 53 | user, password = u.split(':') 54 | opts[:basic_auth] = { username: user, password: password } 55 | end 56 | 57 | o.on("-r", "--response-code", "Command fails if response code >= 400") do 58 | opts[:response_code] = true 59 | end 60 | 61 | o.on("-h", "--help", "Show help documentation") do |h| 62 | puts o 63 | exit 64 | end 65 | 66 | o.on("--version", "Show HTTParty version") do |ver| 67 | puts "Version: #{HTTParty::VERSION}" 68 | exit 69 | end 70 | end.parse! 71 | 72 | if ARGV.empty? 73 | STDERR.puts "You need to provide a URL" 74 | STDERR.puts "USAGE: #{$PROGRAM_NAME} [options] [url]" 75 | end 76 | 77 | def dump_headers(response) 78 | resp_type = Net::HTTPResponse::CODE_TO_OBJ[response.code.to_s] 79 | puts "#{response.code} #{resp_type.to_s.sub(/^Net::HTTP/, '')}" 80 | response.headers.each do |n, v| 81 | puts "#{n}: #{v}" 82 | end 83 | puts 84 | end 85 | 86 | if opts[:verbose] 87 | puts "#{opts[:action].to_s.upcase} #{ARGV.first}" 88 | opts[:headers].each do |n, v| 89 | puts "#{n}: #{v}" 90 | end 91 | puts 92 | end 93 | 94 | response = HTTParty.send(opts[:action], ARGV.first, opts) 95 | if opts[:output_format].nil? 96 | dump_headers(response) if opts[:verbose] 97 | pp response 98 | else 99 | print_format = opts[:output_format] 100 | dump_headers(response) if opts[:verbose] 101 | 102 | case opts[:output_format] 103 | when :json 104 | begin 105 | require 'json' 106 | puts JSON.pretty_generate(response.parsed_response) 107 | rescue LoadError 108 | puts YAML.dump(response) 109 | rescue JSON::JSONError 110 | puts response.inspect 111 | end 112 | when :xml 113 | require 'rexml/document' 114 | REXML::Document.new(response.body).write(STDOUT, 2) 115 | puts 116 | when :csv 117 | require 'csv' 118 | puts CSV.parse(response.body).map(&:to_s) 119 | else 120 | puts response 121 | end 122 | end 123 | exit false if opts[:response_code] && response.code >= 400 124 | -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: features --format progress 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # httparty 2 | 3 | Makes http fun again! 4 | 5 | ## Table of contents 6 | - [Parsing JSON](#parsing-json) 7 | - [Working with SSL](#working-with-ssl) 8 | 9 | ## Parsing JSON 10 | If the response Content Type is `application/json`, HTTParty will parse the response and return Ruby objects such as a hash or array. The default behavior for parsing JSON will return keys as strings. This can be supressed with the `format` option. To get hash keys as symbols: 11 | 12 | ```ruby 13 | response = HTTParty.get('http://example.com', format: :plain) 14 | JSON.parse response, symbolize_names: true 15 | ``` 16 | 17 | ## Posting JSON 18 | When using Content Type `application/json` with `POST`, `PUT` or `PATCH` requests, the body should be a string of valid JSON: 19 | 20 | ```ruby 21 | # With written JSON 22 | HTTParty.post('http://example.com', body: "{\"foo\":\"bar\"}", headers: { 'Content-Type' => 'application/json' }) 23 | 24 | # Using JSON.generate 25 | HTTParty.post('http://example.com', body: JSON.generate({ foo: 'bar' }), headers: { 'Content-Type' => 'application/json' }) 26 | 27 | # Using object.to_json 28 | HTTParty.post('http://example.com', body: { foo: 'bar' }.to_json, headers: { 'Content-Type' => 'application/json' }) 29 | ``` 30 | 31 | ## Working with SSL 32 | 33 | You can use this guide to work with SSL certificates. 34 | 35 | #### Using `pem` option 36 | 37 | ```ruby 38 | # Use this example if you are using a pem file 39 | # - cert.pem must contain the content of a PEM file having the private key appended (separated from the cert by a newline \n) 40 | # - Use an empty string for the password if the cert is not password protected 41 | 42 | class Client 43 | include HTTParty 44 | 45 | base_uri "https://example.com" 46 | pem File.read("#{File.expand_path('.')}/path/to/certs/cert.pem"), "123456" 47 | end 48 | ``` 49 | 50 | #### Using `pkcs12` option 51 | 52 | ```ruby 53 | # Use this example if you are using a pkcs12 file 54 | 55 | class Client 56 | include HTTParty 57 | 58 | base_uri "https://example.com" 59 | pkcs12 File.read("#{File.expand_path('.')}/path/to/certs/cert.p12"), "123456" 60 | end 61 | ``` 62 | 63 | #### Using `ssl_ca_file` option 64 | 65 | ```ruby 66 | # Use this example if you are using a pkcs12 file 67 | 68 | class Client 69 | include HTTParty 70 | 71 | base_uri "https://example.com" 72 | ssl_ca_file "#{File.expand_path('.')}/path/to/certs/cert.pem" 73 | end 74 | ``` 75 | 76 | #### Using `ssl_ca_path` option 77 | 78 | ```ruby 79 | # Use this example if you are using a pkcs12 file 80 | 81 | class Client 82 | include HTTParty 83 | 84 | base_uri "https://example.com" 85 | ssl_ca_path '/path/to/certs' 86 | end 87 | ``` 88 | 89 | You can also include all of these options with the call: 90 | 91 | ```ruby 92 | class Client 93 | include HTTParty 94 | 95 | base_uri "https://example.com" 96 | 97 | def self.fetch 98 | get("/resources", pem: File.read("#{File.expand_path('.')}/path/to/certs/cert.pem"), pem_password: "123456") 99 | end 100 | end 101 | ``` 102 | 103 | ### Avoid SSL verification 104 | 105 | In some cases you may want to skip SSL verification, because the entity that issued the certificate is not a valid one, but you still want to work with it. You can achieve this through: 106 | 107 | ```ruby 108 | # Skips SSL certificate verification 109 | 110 | class Client 111 | include HTTParty 112 | 113 | base_uri "https://example.com" 114 | pem File.read("#{File.expand_path('.')}/path/to/certs/cert.pem"), "123456" 115 | 116 | def self.fetch 117 | get("/resources", verify: false) 118 | # You can also use something like: 119 | # get("resources", verify_peer: false) 120 | end 121 | end 122 | ``` 123 | 124 | ### HTTP Compression 125 | 126 | The `Accept-Encoding` request header and `Content-Encoding` response header 127 | are used to control compression (gzip, etc.) over the wire. Refer to 128 | [RFC-2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) for details. 129 | (For clarity: these headers are **not** used for character encoding i.e. `utf-8` 130 | which is specified in the `Accept` and `Content-Type` headers.) 131 | 132 | Unless you have specific requirements otherwise, we recommend to **not** set 133 | set the `Accept-Encoding` header on HTTParty requests. In this case, `Net::HTTP` 134 | will set a sensible default compression scheme and automatically decompress the response. 135 | 136 | If you explicitly set `Accept-Encoding`, there be dragons: 137 | 138 | * If the HTTP response `Content-Encoding` received on the wire is `gzip` or `deflate`, 139 | `Net::HTTP` will automatically decompress it, and will omit `Content-Encoding` 140 | from your `HTTParty::Response` headers. 141 | 142 | * For the following encodings, HTTParty will automatically decompress them if you include 143 | the required gem into your project. Similar to above, if decompression succeeds, 144 | `Content-Encoding` will be omitted from your `HTTParty::Response` headers. 145 | **Warning:** Support for these encodings is experimental and not fully battle-tested. 146 | 147 | | Content-Encoding | Required Gem | 148 | | --- | --- | 149 | | `br` (Brotli) | [brotli](https://rubygems.org/gems/brotli) | 150 | | `compress` (LZW) | [ruby-lzws](https://rubygems.org/gems/ruby-lzws) | 151 | | `zstd` (Zstandard) | [zstd-ruby](https://rubygems.org/gems/zstd-ruby) | 152 | 153 | * For other encodings, `HTTParty::Response#body` will return the raw uncompressed byte string, 154 | and you'll need to inspect the `Content-Encoding` response header and decompress it yourself. 155 | In this case, `HTTParty::Response#parsed_response` will be `nil`. 156 | 157 | * Lastly, you may use the `skip_decompression` option to disable all automatic decompression 158 | and always get `HTTParty::Response#body` in its raw form along with the `Content-Encoding` header. 159 | 160 | ```ruby 161 | # Accept-Encoding=gzip,deflate can be safely assumed to be auto-decompressed 162 | 163 | res = HTTParty.get('https://example.com/test.json', headers: { 'Accept-Encoding' => 'gzip,deflate,identity' }) 164 | JSON.parse(res.body) # safe 165 | 166 | 167 | # Accept-Encoding=br,compress requires third-party gems 168 | 169 | require 'brotli' 170 | require 'lzws' 171 | require 'zstd-ruby' 172 | res = HTTParty.get('https://example.com/test.json', headers: { 'Accept-Encoding' => 'br,compress,zstd' }) 173 | JSON.parse(res.body) 174 | 175 | 176 | # Accept-Encoding=* may return unhandled Content-Encoding 177 | 178 | res = HTTParty.get('https://example.com/test.json', headers: { 'Accept-Encoding' => '*' }) 179 | encoding = res.headers['Content-Encoding'] 180 | if encoding 181 | JSON.parse(your_decompression_handling(res.body, encoding)) 182 | else 183 | # Content-Encoding not present implies decompressed 184 | JSON.parse(res.body) 185 | end 186 | 187 | 188 | # Gimme the raw data! 189 | 190 | res = HTTParty.get('https://example.com/test.json', skip_decompression: true) 191 | encoding = res.headers['Content-Encoding'] 192 | JSON.parse(your_decompression_handling(res.body, encoding)) 193 | ``` 194 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | * [Amazon Book Search](aaws.rb) 4 | * Httparty included into poro class 5 | * Uses `get` requests 6 | * Transforms query params to uppercased params 7 | 8 | * [Google Search](google.rb) 9 | * Httparty included into poro class 10 | * Uses `get` requests 11 | 12 | * [Crack Custom Parser](crack.rb) 13 | * Creates a custom parser for XML using crack gem 14 | * Uses `get` request 15 | 16 | * [Create HTML Nokogiri parser](nokogiri_html_parser.rb) 17 | * Adds Html as a format 18 | * passed the body of request to Nokogiri 19 | 20 | * [More Custom Parsers](custom_parsers.rb) 21 | * Create an additional parser for atom or make it the ONLY parser 22 | 23 | * [Basic Auth, Delicious](delicious.rb) 24 | * Basic Auth, shows how to merge those into options 25 | * Uses `get` requests 26 | 27 | * [Passing Headers, User Agent](headers_and_user_agents.rb) 28 | * Use the class method of Httparty 29 | * Pass the User-Agent in the headers 30 | * Uses `get` requests 31 | 32 | * [Basic Post Request](basic.rb) 33 | * Httparty included into poro class 34 | * Uses `post` requests 35 | 36 | * [Access Rubyurl Shortener](rubyurl.rb) 37 | * Httparty included into poro class 38 | * Uses `post` requests 39 | 40 | * [Add a custom log file](logging.rb) 41 | * create a log file and have httparty log requests 42 | 43 | * [Accessing StackExchange](stackexchange.rb) 44 | * Httparty included into poro class 45 | * Creates methods for different endpoints 46 | * Uses `get` requests 47 | 48 | * [Accessing Tripit](tripit_sign_in.rb) 49 | * Httparty included into poro class 50 | * Example of using `debug_output` to see headers/urls passed 51 | * Getting and using Cookies 52 | * Uses `get` requests 53 | 54 | * [Accessing Twitter](twitter.rb) 55 | * Httparty included into poro class 56 | * Basic Auth 57 | * Loads settings from a config file 58 | * Uses `get` requests 59 | * Uses `post` requests 60 | 61 | * [Accessing WhoIsMyRep](whoismyrep.rb) 62 | * Httparty included into poro class 63 | * Uses `get` requests 64 | * Two ways to pass params to get, inline on the url or in query hash 65 | 66 | * [Rescue Json Error](rescue_json.rb) 67 | * Rescue errors due to parsing response 68 | 69 | * [Download file using stream mode](stream_download.rb) 70 | * Uses `get` requests 71 | * Uses `stream_body` mode 72 | * Download file without using the memory 73 | 74 | * [Microsoft graph](microsoft_graph.rb) 75 | * Basic Auth 76 | * Uses `post` requests 77 | * Uses multipart 78 | 79 | * [Multipart](multipart.rb) 80 | * Multipart data upload _(with and without file)_ 81 | 82 | * [Uploading File](body_stream.rb) 83 | * Uses `body_stream` to upload file 84 | 85 | * [Accessing x509 Peer Certificate](peer_cert.rb) 86 | * Provides access to the server's TLS certificate 87 | 88 | * [Accessing IDNs](idn.rb) 89 | * Uses a `get` request with an International domain names, which are Urls with emojis and non-ASCII characters such as accented letters. -------------------------------------------------------------------------------- /examples/aaws.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'active_support' 3 | require 'active_support/core_ext/hash' 4 | require 'active_support/core_ext/string' 5 | 6 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | require File.join(dir, 'httparty') 8 | require 'pp' 9 | config = YAML.load(File.read(File.join(ENV['HOME'], '.aaws'))) 10 | 11 | module AAWS 12 | class Book 13 | include HTTParty 14 | base_uri 'http://ecs.amazonaws.com' 15 | default_params Service: 'AWSECommerceService', Operation: 'ItemSearch', SearchIndex: 'Books' 16 | 17 | def initialize(key) 18 | @auth = { AWSAccessKeyId: key } 19 | end 20 | 21 | def search(options = {}) 22 | raise ArgumentError, 'You must search for something' if options[:query].blank? 23 | 24 | # amazon uses nasty camelized query params 25 | options[:query] = options[:query] 26 | .reverse_merge(@auth) 27 | .transform_keys { |k| k.to_s.camelize } 28 | 29 | # make a request and return the items (NOTE: this doesn't handle errors at this point) 30 | self.class.get('/onca/xml', options)['ItemSearchResponse']['Items'] 31 | end 32 | end 33 | end 34 | 35 | aaws = AAWS::Book.new(config[:access_key]) 36 | pp aaws.search(query: { title: 'Ruby On Rails' }) 37 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | # You can also use post, put, delete, head, options in the same fashion 6 | response = HTTParty.get('https://api.stackexchange.com/2.2/questions?site=stackoverflow') 7 | puts response.body, response.code, response.message, response.headers.inspect 8 | 9 | # An example post to a minimal rails app in the development environment 10 | # Note that "skip_before_filter :verify_authenticity_token" must be set in the 11 | # "pears" controller for this example 12 | 13 | class Partay 14 | include HTTParty 15 | base_uri 'http://localhost:3000' 16 | end 17 | 18 | options = { 19 | body: { 20 | pear: { # your resource 21 | foo: '123', # your columns/data 22 | bar: 'second', 23 | baz: 'last thing' 24 | } 25 | } 26 | } 27 | 28 | pp Partay.post('/pears.xml', options) 29 | -------------------------------------------------------------------------------- /examples/body_stream.rb: -------------------------------------------------------------------------------- 1 | # To upload file to a server use :body_stream 2 | 3 | HTTParty.put( 4 | 'http://localhost:3000/train', 5 | body_stream: File.open('sample_configs/config_train_server_md.yml', 'r') 6 | ) 7 | 8 | 9 | # Actually, it works with any IO object 10 | 11 | HTTParty.put( 12 | 'http://localhost:3000/train', 13 | body_stream: StringIO.new('foo') 14 | ) 15 | -------------------------------------------------------------------------------- /examples/crack.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'crack' 3 | 4 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require File.join(dir, 'httparty') 6 | require 'pp' 7 | 8 | class Rep 9 | include HTTParty 10 | 11 | parser( 12 | proc do |body, format| 13 | Crack::XML.parse(body) 14 | end 15 | ) 16 | end 17 | 18 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php?zip=46544') 19 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php', query: { zip: 46544 }) 20 | -------------------------------------------------------------------------------- /examples/custom_parsers.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class ParseAtom 6 | include HTTParty 7 | 8 | # Support Atom along with the default parsers: xml, json, etc. 9 | class Parser::Atom < HTTParty::Parser 10 | SupportedFormats.merge!({"application/atom+xml" => :atom}) 11 | 12 | protected 13 | 14 | # perform atom parsing on body 15 | def atom 16 | body.to_atom 17 | end 18 | end 19 | 20 | parser Parser::Atom 21 | end 22 | 23 | class OnlyParseAtom 24 | include HTTParty 25 | 26 | # Only support Atom 27 | class Parser::OnlyAtom < HTTParty::Parser 28 | SupportedFormats = { "application/atom+xml" => :atom } 29 | 30 | protected 31 | 32 | # perform atom parsing on body 33 | def atom 34 | body.to_atom 35 | end 36 | end 37 | 38 | parser Parser::OnlyAtom 39 | end 40 | 41 | class SkipParsing 42 | include HTTParty 43 | 44 | # Parse the response body however you like 45 | class Parser::Simple < HTTParty::Parser 46 | def parse 47 | body 48 | end 49 | end 50 | 51 | parser Parser::Simple 52 | end 53 | 54 | class AdHocParsing 55 | include HTTParty 56 | parser( 57 | proc do |body, format| 58 | case format 59 | when :json 60 | body.to_json 61 | when :xml 62 | body.to_xml 63 | else 64 | body 65 | end 66 | end 67 | ) 68 | end 69 | -------------------------------------------------------------------------------- /examples/delicious.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | config = YAML.load(File.read(File.join(ENV['HOME'], '.delicious'))) 5 | 6 | class Delicious 7 | include HTTParty 8 | base_uri 'https://api.del.icio.us/v1' 9 | 10 | def initialize(u, p) 11 | @auth = { username: u, password: p } 12 | end 13 | 14 | # query params that filter the posts are: 15 | # tag (optional). Filter by this tag. 16 | # dt (optional). Filter by this date (CCYY-MM-DDThh:mm:ssZ). 17 | # url (optional). Filter by this url. 18 | # ie: posts(query: {tag: 'ruby'}) 19 | def posts(options = {}) 20 | options.merge!({ basic_auth: @auth }) 21 | self.class.get('/posts/get', options) 22 | end 23 | 24 | # query params that filter the posts are: 25 | # tag (optional). Filter by this tag. 26 | # count (optional). Number of items to retrieve (Default:15, Maximum:100). 27 | def recent(options = {}) 28 | options.merge!({ basic_auth: @auth }) 29 | self.class.get('/posts/recent', options) 30 | end 31 | end 32 | 33 | delicious = Delicious.new(config['username'], config['password']) 34 | pp delicious.posts(query: { tag: 'ruby' }) 35 | pp delicious.recent 36 | 37 | delicious.recent['posts']['post'].each { |post| puts post['href'] } 38 | -------------------------------------------------------------------------------- /examples/google.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Google 6 | include HTTParty 7 | format :html 8 | end 9 | 10 | # google.com redirects to www.google.com so this is live test for redirection 11 | pp Google.get('http://google.com') 12 | 13 | puts '', '*' * 70, '' 14 | 15 | # check that ssl is requesting right 16 | pp Google.get('https://www.google.com') 17 | -------------------------------------------------------------------------------- /examples/headers_and_user_agents.rb: -------------------------------------------------------------------------------- 1 | # To send custom user agents to identify your application to a web service (or mask as a specific browser for testing), send "User-Agent" as a hash to headers as shown below. 2 | 3 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | require File.join(dir, 'httparty') 5 | require 'pp' 6 | 7 | response = HTTParty.get('http://example.com', { 8 | headers: {"User-Agent" => "Httparty"}, 9 | debug_output: STDOUT, # To show that User-Agent is Httparty 10 | }) 11 | -------------------------------------------------------------------------------- /examples/idn.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Idn 6 | include HTTParty 7 | uri_adapter Addressable::URI 8 | end 9 | 10 | pp Idn.get("https://i❤️.ws/emojidomain/💎?format=json") -------------------------------------------------------------------------------- /examples/logging.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'logger' 4 | require 'pp' 5 | 6 | my_logger = Logger.new STDOUT 7 | 8 | my_logger.info "Logging can be used on the main HTTParty class. It logs redirects too." 9 | HTTParty.get "http://google.com", logger: my_logger 10 | 11 | my_logger.info '*' * 70 12 | 13 | my_logger.info "It can be used also on a custom class." 14 | 15 | class Google 16 | include HTTParty 17 | logger ::Logger.new STDOUT 18 | end 19 | 20 | Google.get "http://google.com" 21 | 22 | my_logger.info '*' * 70 23 | 24 | my_logger.info "The default formatter is :apache. The :curl formatter can also be used." 25 | my_logger.info "You can tell which method to call on the logger too. It is info by default." 26 | HTTParty.get "http://google.com", logger: my_logger, log_level: :debug, log_format: :curl 27 | 28 | my_logger.info '*' * 70 29 | 30 | my_logger.info "These configs are also available on custom classes." 31 | class Google 32 | include HTTParty 33 | logger ::Logger.new(STDOUT), :debug, :curl 34 | end 35 | 36 | Google.get "http://google.com" 37 | -------------------------------------------------------------------------------- /examples/microsoft_graph.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | 3 | class MicrosoftGraph 4 | MS_BASE_URL = "https://login.microsoftonline.com".freeze 5 | TOKEN_REQUEST_PATH = "oauth2/v2.0/token".freeze 6 | 7 | def initialize(tenant_id) 8 | @tenant_id = tenant_id 9 | end 10 | 11 | # Make a request to the Microsoft Graph API, for instance https://graph.microsoft.com/v1.0/users 12 | def request(url) 13 | return false unless (token = bearer_token) 14 | 15 | response = HTTParty.get( 16 | url, 17 | headers: { 18 | Authorization: "Bearer #{token}" 19 | } 20 | ) 21 | 22 | return false unless response.code == 200 23 | 24 | return JSON.parse(response.body) 25 | end 26 | 27 | private 28 | 29 | # A post to the Microsoft Graph to get a bearer token for the specified tenant. In this example 30 | # our Rails application has already been given permission to request these tokens by the admin of 31 | # the specified tenant_id. 32 | # 33 | # See here for more information https://developer.microsoft.com/en-us/graph/docs/concepts/auth_v2_service 34 | # 35 | # This request also makes use of the multipart/form-data post body. 36 | def bearer_token 37 | response = HTTParty.post( 38 | "#{MS_BASE_URL}/#{@tenant_id}/#{TOKEN_REQUEST_PATH}", 39 | multipart: true, 40 | body: { 41 | client_id: Rails.application.credentials[Rails.env.to_sym][:microsoft_client_id], 42 | client_secret: Rails.application.credentials[Rails.env.to_sym][:microsoft_client_secret], 43 | scope: 'https://graph.microsoft.com/.default', 44 | grant_type: 'client_credentials' 45 | } 46 | ) 47 | 48 | return false unless response.code == 200 49 | 50 | JSON.parse(response.body)['access_token'] 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /examples/multipart.rb: -------------------------------------------------------------------------------- 1 | # If you are uploading file in params, multipart will used as content-type automatically 2 | 3 | HTTParty.post( 4 | 'http://localhost:3000/user', 5 | body: { 6 | name: 'Foo Bar', 7 | email: 'example@email.com', 8 | avatar: File.open('/full/path/to/avatar.jpg') 9 | } 10 | ) 11 | 12 | 13 | # However, you can force it yourself 14 | 15 | HTTParty.post( 16 | 'http://localhost:3000/user', 17 | multipart: true, 18 | body: { 19 | name: 'Foo Bar', 20 | email: 'example@email.com' 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /examples/nokogiri_html_parser.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'nokogiri' 3 | 4 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require File.join(dir, 'httparty') 6 | require 'pp' 7 | 8 | class HtmlParserIncluded < HTTParty::Parser 9 | def html 10 | Nokogiri::HTML(body) 11 | end 12 | end 13 | 14 | class Page 15 | include HTTParty 16 | parser HtmlParserIncluded 17 | end 18 | 19 | pp Page.get('http://www.google.com') 20 | -------------------------------------------------------------------------------- /examples/party_foul_mode.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | 3 | class APIClient 4 | include HTTParty 5 | base_uri 'api.example.com' 6 | 7 | def self.fetch_user(id) 8 | begin 9 | get("/users/#{id}", foul: true) 10 | rescue HTTParty::NetworkError => e 11 | handle_network_error(e) 12 | rescue HTTParty::ResponseError => e 13 | handle_api_error(e) 14 | end 15 | end 16 | 17 | private 18 | 19 | def self.handle_network_error(error) 20 | case error.cause 21 | when Errno::ECONNREFUSED 22 | { 23 | error: :server_down, 24 | message: "The API server appears to be down", 25 | details: error.message 26 | } 27 | when Net::OpenTimeout, Timeout::Error 28 | { 29 | error: :timeout, 30 | message: "The request timed out", 31 | details: error.message 32 | } 33 | when SocketError 34 | { 35 | error: :network_error, 36 | message: "Could not connect to the API server", 37 | details: error.message 38 | } 39 | when OpenSSL::SSL::SSLError 40 | { 41 | error: :ssl_error, 42 | message: "SSL certificate verification failed", 43 | details: error.message 44 | } 45 | else 46 | { 47 | error: :unknown_network_error, 48 | message: "An unexpected network error occurred", 49 | details: error.message 50 | } 51 | end 52 | end 53 | 54 | def self.handle_api_error(error) 55 | { 56 | error: :api_error, 57 | message: "API returned error #{error.response.code}", 58 | details: error.response.body 59 | } 60 | end 61 | end 62 | 63 | # Example usage: 64 | 65 | # 1. When server is down 66 | result = APIClient.fetch_user(123) 67 | puts "Server down example:" 68 | puts result.inspect 69 | puts 70 | 71 | # 2. When request times out 72 | result = APIClient.fetch_user(456) 73 | puts "Timeout example:" 74 | puts result.inspect 75 | puts 76 | 77 | # 3. When SSL error occurs 78 | result = APIClient.fetch_user(789) 79 | puts "SSL error example:" 80 | puts result.inspect 81 | puts 82 | 83 | # 4. Simple example without a wrapper class 84 | begin 85 | HTTParty.get('https://api.example.com/users', foul: true) 86 | rescue HTTParty::Foul => e 87 | puts "Direct usage example:" 88 | puts "Error type: #{e.cause.class}" 89 | puts "Error message: #{e.message}" 90 | end 91 | -------------------------------------------------------------------------------- /examples/peer_cert.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | 4 | peer_cert = nil 5 | HTTParty.get("https://www.example.com") do |fragment| 6 | peer_cert ||= fragment.connection.peer_cert 7 | end 8 | 9 | puts "The server's certificate expires #{peer_cert.not_after}" 10 | -------------------------------------------------------------------------------- /examples/rescue_json.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | 4 | # Take note of the "; 1" at the end of the following line. It's required only if 5 | # running this in IRB, because IRB will try to inspect the variable named 6 | # "request", triggering the exception. 7 | request = HTTParty.get 'https://rubygems.org/api/v1/versions/doesnotexist.json' ; 1 8 | 9 | # Check an exception due to parsing the response 10 | # because HTTParty evaluate the response lazily 11 | begin 12 | request.inspect 13 | # This would also suffice by forcing the request to be parsed: 14 | # request.parsed_response 15 | rescue => e 16 | puts "Rescued #{e.inspect}" 17 | end 18 | -------------------------------------------------------------------------------- /examples/rubyurl.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Rubyurl 6 | include HTTParty 7 | base_uri 'rubyurl.com' 8 | 9 | def self.shorten(website_url) 10 | post('/api/links.json', query: { link: { website_url: website_url } }) 11 | end 12 | end 13 | 14 | pp Rubyurl.shorten('http://istwitterdown.com/') 15 | -------------------------------------------------------------------------------- /examples/stackexchange.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class StackExchange 6 | include HTTParty 7 | base_uri 'api.stackexchange.com' 8 | 9 | def initialize(service, page) 10 | @options = { query: { site: service, page: page } } 11 | end 12 | 13 | def questions 14 | self.class.get("/2.2/questions", @options) 15 | end 16 | 17 | def users 18 | self.class.get("/2.2/users", @options) 19 | end 20 | end 21 | 22 | stack_exchange = StackExchange.new("stackoverflow", 1) 23 | pp stack_exchange.questions 24 | pp stack_exchange.users 25 | -------------------------------------------------------------------------------- /examples/stream_download.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | # download file linux-4.6.4.tar.xz without using the memory 6 | response = nil 7 | filename = "linux-4.6.4.tar.xz" 8 | url = "https://cdn.kernel.org/pub/linux/kernel/v4.x/#{filename}" 9 | 10 | File.open(filename, "w") do |file| 11 | response = HTTParty.get(url, stream_body: true) do |fragment| 12 | if [301, 302].include?(fragment.code) 13 | print "skip writing for redirect" 14 | elsif fragment.code == 200 15 | print "." 16 | file.write(fragment) 17 | else 18 | raise StandardError, "Non-success status code while streaming #{fragment.code}" 19 | end 20 | end 21 | end 22 | puts 23 | 24 | pp "Success: #{response.success?}" 25 | pp File.stat(filename).inspect 26 | File.unlink(filename) 27 | -------------------------------------------------------------------------------- /examples/tripit_sign_in.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | 4 | class TripIt 5 | include HTTParty 6 | base_uri 'https://www.tripit.com' 7 | debug_output 8 | 9 | def initialize(email, password) 10 | @email = email 11 | get_response = self.class.get('/account/login') 12 | get_response_cookie = parse_cookie(get_response.headers['Set-Cookie']) 13 | 14 | post_response = self.class.post( 15 | '/account/login', 16 | body: { 17 | login_email_address: email, 18 | login_password: password 19 | }, 20 | headers: {'Cookie' => get_response_cookie.to_cookie_string } 21 | ) 22 | 23 | @cookie = parse_cookie(post_response.headers['Set-Cookie']) 24 | end 25 | 26 | def account_settings 27 | self.class.get('/account/edit', headers: { 'Cookie' => @cookie.to_cookie_string }) 28 | end 29 | 30 | def logged_in? 31 | account_settings.include? "You're logged in as #{@email}" 32 | end 33 | 34 | private 35 | 36 | def parse_cookie(resp) 37 | cookie_hash = CookieHash.new 38 | resp.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) } 39 | cookie_hash 40 | end 41 | end 42 | 43 | tripit = TripIt.new('email', 'password') 44 | puts "Logged in: #{tripit.logged_in?}" 45 | -------------------------------------------------------------------------------- /examples/twitter.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | config = YAML.load(File.read(File.join(ENV['HOME'], '.twitter'))) 5 | 6 | class Twitter 7 | include HTTParty 8 | base_uri 'twitter.com' 9 | 10 | def initialize(u, p) 11 | @auth = {username: u, password: p} 12 | end 13 | 14 | # which can be :friends, :user or :public 15 | # options[:query] can be things like since, since_id, count, etc. 16 | def timeline(which = :friends, options = {}) 17 | options.merge!({ basic_auth: @auth }) 18 | self.class.get("/statuses/#{which}_timeline.json", options) 19 | end 20 | 21 | def post(text) 22 | options = { query: { status: text }, basic_auth: @auth } 23 | self.class.post('/statuses/update.json', options) 24 | end 25 | end 26 | 27 | twitter = Twitter.new(config['email'], config['password']) 28 | pp twitter.timeline 29 | # pp twitter.timeline(:friends, query: {since_id: 868482746}) 30 | # pp twitter.timeline(:friends, query: 'since_id=868482746') 31 | # pp twitter.post('this is a test of 0.2.0') 32 | -------------------------------------------------------------------------------- /examples/whoismyrep.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require File.join(dir, 'httparty') 3 | require 'pp' 4 | 5 | class Rep 6 | include HTTParty 7 | end 8 | 9 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php?zip=46544') 10 | pp Rep.get('http://whoismyrepresentative.com/getall_mems.php', query: { zip: 46544 }) 11 | -------------------------------------------------------------------------------- /features/basic_authentication.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic Authentication 2 | 3 | As a developer 4 | I want to be able to use a service that requires Basic Authentication 5 | Because that is not an uncommon requirement 6 | 7 | Scenario: Passing no credentials to a page requiring Basic Authentication 8 | Given a restricted page at '/basic_auth.html' 9 | When I call HTTParty#get with '/basic_auth.html' 10 | Then it should return a response with a 401 response code 11 | 12 | Scenario: Passing proper credentials to a page requiring Basic Authentication 13 | Given a remote service that returns 'Authenticated Page' 14 | And that service is accessed at the path '/basic_auth.html' 15 | And that service is protected by Basic Authentication 16 | And that service requires the username 'jcash' with the password 'maninblack' 17 | When I call HTTParty#get with '/basic_auth.html' and a basic_auth hash: 18 | | username | password | 19 | | jcash | maninblack | 20 | Then the return value should match 'Authenticated Page' 21 | -------------------------------------------------------------------------------- /features/command_line.feature: -------------------------------------------------------------------------------- 1 | @command_line 2 | Feature: Command Line 3 | 4 | As a developer 5 | I want to be able to harness the power of HTTParty from the command line 6 | Because that would make quick testing and debugging easy 7 | 8 | Scenario: Show help information 9 | When I run `httparty --help` 10 | Then the output should contain "-f, --format [FORMAT]" 11 | 12 | Scenario: Show current version 13 | When I run `httparty --version` 14 | Then the output should contain "Version:" 15 | And the output should not contain "You need to provide a URL" 16 | 17 | Scenario: Make a get request 18 | Given a remote deflate service on port '4001' 19 | And the response from the service has a body of 'GET request' 20 | And that service is accessed at the path '/fun' 21 | When I run `httparty http://0.0.0.0:4001/fun` 22 | Then the output should contain "GET request" 23 | 24 | Scenario: Make a post request 25 | Given a remote deflate service on port '4002' 26 | And the response from the service has a body of 'POST request' 27 | And that service is accessed at the path '/fun' 28 | When I run `httparty http://0.0.0.0:4002/fun --action post --data "a=1&b=2"` 29 | Then the output should contain "POST request" 30 | 31 | Scenario: Make a put request 32 | Given a remote deflate service on port '4003' 33 | And the response from the service has a body of 'PUT request' 34 | And that service is accessed at the path '/fun' 35 | When I run `httparty http://0.0.0.0:4003/fun --action put --data "a=1&b=2"` 36 | Then the output should contain "PUT request" 37 | 38 | Scenario: Make a delete request 39 | Given a remote deflate service on port '4004' 40 | And the response from the service has a body of 'DELETE request' 41 | And that service is accessed at the path '/fun' 42 | When I run `httparty http://0.0.0.0:4004/fun --action delete` 43 | Then the output should contain "DELETE request" 44 | 45 | Scenario: Set a verbose mode 46 | Given a remote deflate service on port '4005' 47 | And the response from the service has a body of 'Some request' 48 | And that service is accessed at the path '/fun' 49 | When I run `httparty http://0.0.0.0:4005/fun --verbose` 50 | Then the output should contain "content-length" 51 | 52 | Scenario: Use service with basic authentication 53 | Given a remote deflate service on port '4006' 54 | And the response from the service has a body of 'Successfull authentication' 55 | And that service is accessed at the path '/fun' 56 | And that service is protected by Basic Authentication 57 | And that service requires the username 'user' with the password 'pass' 58 | When I run `httparty http://0.0.0.0:4006/fun --user 'user:pass'` 59 | Then the output should contain "Successfull authentication" 60 | 61 | Scenario: Get response in plain format 62 | Given a remote deflate service on port '4007' 63 | And the response from the service has a body of 'Some request' 64 | And that service is accessed at the path '/fun' 65 | When I run `httparty http://0.0.0.0:4007/fun --format plain` 66 | Then the output should contain "Some request" 67 | 68 | Scenario: Get response in json format 69 | Given a remote deflate service on port '4008' 70 | Given a remote service that returns '{ "jennings": "waylon", "cash": "johnny" }' 71 | And that service is accessed at the path '/service.json' 72 | And the response from the service has a Content-Type of 'application/json' 73 | When I run `httparty http://0.0.0.0:4008/service.json --format json` 74 | Then the output should contain '"jennings": "waylon"' 75 | 76 | Scenario: Get response in xml format 77 | Given a remote deflate service on port '4009' 78 | Given a remote service that returns 'waylon jennings' 79 | And that service is accessed at the path '/service.xml' 80 | And the response from the service has a Content-Type of 'text/xml' 81 | When I run `httparty http://0.0.0.0:4009/service.xml --format xml` 82 | Then the output should contain "" 83 | 84 | Scenario: Get response in csv format 85 | Given a remote deflate service on port '4010' 86 | Given a remote service that returns: 87 | """ 88 | "Last Name","Name" 89 | "jennings","waylon" 90 | "cash","johnny" 91 | """ 92 | And that service is accessed at the path '/service.csv' 93 | And the response from the service has a Content-Type of 'application/csv' 94 | When I run `httparty http://0.0.0.0:4010/service.csv --format csv` 95 | Then the output should contain '["Last Name", "Name"]' 96 | -------------------------------------------------------------------------------- /features/deals_with_http_error_codes.feature: -------------------------------------------------------------------------------- 1 | Feature: Deals with HTTP error codes 2 | 3 | As a developer 4 | I want to be informed of non-successful responses 5 | Because sometimes thing explode 6 | And I should probably know what happened 7 | 8 | Scenario: A response of '404 - Not Found' 9 | Given a remote service that returns a 404 status code 10 | And that service is accessed at the path '/404_service.html' 11 | When I call HTTParty#get with '/404_service.html' 12 | Then it should return a response with a 404 response code 13 | 14 | Scenario: A response of '500 - Internal Server Error' 15 | Given a remote service that returns a 500 status code 16 | And that service is accessed at the path '/500_service.html' 17 | When I call HTTParty#get with '/500_service.html' 18 | Then it should return a response with a 500 response code 19 | 20 | Scenario: A non-successful response where I need the body 21 | Given a remote service that returns a 400 status code 22 | And the response from the service has a body of 'Bad response' 23 | And that service is accessed at the path '/400_service.html' 24 | When I call HTTParty#get with '/400_service.html' 25 | Then it should return a response with a 400 response code 26 | And the return value should match 'Bad response' 27 | -------------------------------------------------------------------------------- /features/digest_authentication.feature: -------------------------------------------------------------------------------- 1 | Feature: Digest Authentication 2 | 3 | As a developer 4 | I want to be able to use a service that requires Digest Authentication 5 | Because that is not an uncommon requirement 6 | 7 | Scenario: Passing no credentials to a page requiring Digest Authentication 8 | Given a restricted page at '/digest_auth.html' 9 | When I call HTTParty#get with '/digest_auth.html' 10 | Then it should return a response with a 401 response code 11 | 12 | Scenario: Passing proper credentials to a page requiring Digest Authentication 13 | Given a remote service that returns 'Digest Authenticated Page' 14 | And that service is accessed at the path '/digest_auth.html' 15 | And that service is protected by Digest Authentication 16 | And that service requires the username 'jcash' with the password 'maninblack' 17 | When I call HTTParty#get with '/digest_auth.html' and a digest_auth hash: 18 | | username | password | 19 | | jcash | maninblack | 20 | Then the return value should match 'Digest Authenticated Page' 21 | 22 | Scenario: Passing proper credentials to a page requiring Digest Authentication using md5-sess algorithm 23 | Given a remote service that returns 'Digest Authenticated Page Using MD5-sess' 24 | And that service is accessed at the path '/digest_auth.html' 25 | And that service is protected by MD5-sess Digest Authentication 26 | And that service requires the username 'jcash' with the password 'maninblack' 27 | When I call HTTParty#get with '/digest_auth.html' and a digest_auth hash: 28 | | username | password | 29 | | jcash | maninblack | 30 | Then the return value should match 'Digest Authenticated Page Using MD5-sess' 31 | -------------------------------------------------------------------------------- /features/handles_compressed_responses.feature: -------------------------------------------------------------------------------- 1 | Feature: Handles Compressed Responses 2 | 3 | In order to save bandwidth 4 | As a developer 5 | I want to leverage Net::Http's built in transparent support for gzip and deflate content encoding 6 | 7 | Scenario: Supports deflate encoding 8 | Given a remote deflate service 9 | And the response from the service has a body of '

Some HTML

' 10 | And that service is accessed at the path '/deflate_service.html' 11 | When I call HTTParty#get with '/deflate_service.html' 12 | Then the return value should match '

Some HTML

' 13 | And it should return a response without a content-encoding 14 | 15 | Scenario: Supports gzip encoding 16 | Given a remote gzip service 17 | And the response from the service has a body of '

Some HTML

' 18 | And that service is accessed at the path '/gzip_service.html' 19 | When I call HTTParty#get with '/gzip_service.html' 20 | Then the return value should match '

Some HTML

' 21 | And it should return a response without a content-encoding 22 | 23 | Scenario: Supports gzip encoding with explicit header set 24 | Given a remote gzip service 25 | And the response from the service has a body of '

Some HTML

' 26 | And that service is accessed at the path '/gzip_service.html' 27 | When I set my HTTParty header 'User-Agent' to value 'Party' 28 | And I call HTTParty#get with '/gzip_service.html' 29 | Then the return value should match '

Some HTML

' 30 | And it should return a response without a content-encoding 31 | 32 | Scenario: Supports deflate encoding with explicit header set 33 | Given a remote deflate service 34 | And the response from the service has a body of '

Some HTML

' 35 | And that service is accessed at the path '/deflate_service.html' 36 | When I set my HTTParty header 'User-Agent' to value 'Party' 37 | And I call HTTParty#get with '/deflate_service.html' 38 | Then the return value should match '

Some HTML

' 39 | And it should return a response without a content-encoding 40 | 41 | -------------------------------------------------------------------------------- /features/handles_multiple_formats.feature: -------------------------------------------------------------------------------- 1 | Feature: Handles Multiple Formats 2 | 3 | As a developer 4 | I want to be able to consume remote services of many different formats 5 | And I want those formats to be automatically detected and handled 6 | Because web services take many forms 7 | And I don't want to have to do any extra work 8 | 9 | Scenario: An HTML service 10 | Given a remote service that returns '

Some HTML

' 11 | And that service is accessed at the path '/html_service.html' 12 | And the response from the service has a Content-Type of 'text/html' 13 | When I call HTTParty#get with '/html_service.html' 14 | Then it should return a String 15 | And the return value should match '

Some HTML

' 16 | 17 | Scenario: A CSV service 18 | Given a remote service that returns: 19 | """ 20 | "Last Name","Name" 21 | "jennings","waylon" 22 | "cash","johnny" 23 | """ 24 | And that service is accessed at the path '/service.csv' 25 | And the response from the service has a Content-Type of 'application/csv' 26 | When I call HTTParty#get with '/service.csv' 27 | Then it should return an Array equaling: 28 | | Last Name | Name | 29 | | jennings | waylon | 30 | | cash | johnny | 31 | 32 | Scenario: A JSON service 33 | Given a remote service that returns '{ "jennings": "waylon", "cash": "johnny" }' 34 | And that service is accessed at the path '/service.json' 35 | And the response from the service has a Content-Type of 'application/json' 36 | When I call HTTParty#get with '/service.json' 37 | Then it should return a Hash equaling: 38 | | key | value | 39 | | jennings | waylon | 40 | | cash | johnny | 41 | 42 | Scenario: An XML Service 43 | Given a remote service that returns 'waylon jennings' 44 | And that service is accessed at the path '/service.xml' 45 | And the response from the service has a Content-Type of 'text/xml' 46 | When I call HTTParty#get with '/service.xml' 47 | Then it should return a Hash equaling: 48 | | key | value | 49 | | singer | waylon jennings | 50 | 51 | Scenario: A Javascript remote file 52 | Given a remote service that returns '$(function() { alert("hi"); });' 53 | And that service is accessed at the path '/service.js' 54 | And the response from the service has a Content-Type of 'application/javascript' 55 | When I call HTTParty#get with '/service.js' 56 | Then it should return a String 57 | And the return value should match '$(function() { alert("hi"); });' 58 | -------------------------------------------------------------------------------- /features/steps/env.rb: -------------------------------------------------------------------------------- 1 | require 'mongrel' 2 | require './lib/httparty' 3 | require 'rspec/expectations' 4 | require 'aruba/cucumber' 5 | 6 | def run_server(port) 7 | @host_and_port = "0.0.0.0:#{port}" 8 | @server = Mongrel::HttpServer.new("0.0.0.0", port) 9 | @server.run 10 | @request_options = {} 11 | end 12 | 13 | def new_port 14 | server = TCPServer.new('0.0.0.0', nil) 15 | port = server.addr[1] 16 | ensure 17 | server.close 18 | end 19 | 20 | Before('~@command_line') do 21 | port = ENV["HTTPARTY_PORT"] || new_port 22 | run_server(port) 23 | end 24 | 25 | After do 26 | @server.stop if @server 27 | end 28 | -------------------------------------------------------------------------------- /features/steps/httparty_response_steps.rb: -------------------------------------------------------------------------------- 1 | # Not needed anymore in ruby 2.0, but needed to resolve constants 2 | # in nested namespaces. This is taken from rails :) 3 | def constantize(camel_cased_word) 4 | names = camel_cased_word.split('::') 5 | names.shift if names.empty? || names.first.empty? 6 | 7 | constant = Object 8 | names.each do |name| 9 | constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) 10 | end 11 | constant 12 | end 13 | 14 | Then /it should return an? ([\w\:]+)$/ do |class_string| 15 | expect(@response_from_httparty.parsed_response).to be_a(Object.const_get(class_string)) 16 | end 17 | 18 | Then /the return value should match '(.*)'/ do |expected_text| 19 | expect(@response_from_httparty.parsed_response).to eq(expected_text) 20 | end 21 | 22 | Then /it should return a Hash equaling:/ do |hash_table| 23 | expect(@response_from_httparty.parsed_response).to be_a(Hash) 24 | expect(@response_from_httparty.keys.length).to eq(hash_table.rows.length) 25 | hash_table.hashes.each do |pair| 26 | key, value = pair["key"], pair["value"] 27 | expect(@response_from_httparty.keys).to include(key) 28 | expect(@response_from_httparty[key]).to eq(value) 29 | end 30 | end 31 | 32 | Then /it should return an Array equaling:/ do |array| 33 | expect(@response_from_httparty.parsed_response).to be_a(Array) 34 | expect(@response_from_httparty.parsed_response).to eq(array.raw) 35 | end 36 | 37 | Then /it should return a response with a (\d+) response code/ do |code| 38 | expect(@response_from_httparty.code).to eq(code.to_i) 39 | end 40 | 41 | Then /it should return a response without a content\-encoding$/ do 42 | expect(@response_from_httparty.headers['content-encoding']).to be_nil 43 | end 44 | 45 | Then /it should raise (?:an|a) ([\w:]+) exception/ do |exception| 46 | expect(@exception_from_httparty).to_not be_nil 47 | expect(@exception_from_httparty).to be_a constantize(exception) 48 | end 49 | 50 | Then /it should not raise (?:an|a) ([\w:]+) exception/ do |exception| 51 | expect(@exception_from_httparty).to be_nil 52 | end 53 | -------------------------------------------------------------------------------- /features/steps/httparty_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I set my HTTParty timeout option to (\d+)$/ do |timeout| 2 | @request_options[:timeout] = timeout.to_i 3 | end 4 | 5 | When /^I set my HTTParty open_timeout option to (\d+)$/ do |timeout| 6 | @request_options[:open_timeout] = timeout.to_i 7 | end 8 | 9 | When /^I set my HTTParty read_timeout option to (\d+)$/ do |timeout| 10 | @request_options[:read_timeout] = timeout.to_i 11 | end 12 | 13 | When /^I set my HTTParty header '(.*)' to value '(.*)'$/ do |name, value| 14 | @request_options[:headers] ||= {} 15 | @request_options[:headers][name] = value 16 | end 17 | 18 | When /I set my HTTParty logger option/ do 19 | # TODO: make the IO something portable 20 | @request_options[:logger] = Logger.new("/dev/null") 21 | end 22 | 23 | When /I set my HTTParty parser option to a proc/ do 24 | @request_options[:parser] = proc { |body| body } 25 | end 26 | 27 | When /I call HTTParty#get with '(.*)'$/ do |url| 28 | begin 29 | @response_from_httparty = HTTParty.get("http://#{@host_and_port}#{url}", @request_options) 30 | rescue HTTParty::RedirectionTooDeep, Timeout::Error => e 31 | @exception_from_httparty = e 32 | end 33 | end 34 | 35 | When /^I call HTTParty#head with '(.*)'$/ do |url| 36 | begin 37 | @response_from_httparty = HTTParty.head("http://#{@host_and_port}#{url}", @request_options) 38 | rescue HTTParty::RedirectionTooDeep, Timeout::Error => e 39 | @exception_from_httparty = e 40 | end 41 | end 42 | 43 | When /I call HTTParty#get with '(.*)' and a basic_auth hash:/ do |url, auth_table| 44 | h = auth_table.hashes.first 45 | @response_from_httparty = HTTParty.get( 46 | "http://#{@host_and_port}#{url}", 47 | basic_auth: { username: h["username"], password: h["password"] } 48 | ) 49 | end 50 | 51 | When /I call HTTParty#get with '(.*)' and a digest_auth hash:/ do |url, auth_table| 52 | h = auth_table.hashes.first 53 | @response_from_httparty = HTTParty.get( 54 | "http://#{@host_and_port}#{url}", 55 | digest_auth: { username: h["username"], password: h["password"] } 56 | ) 57 | end 58 | 59 | When /I call Marshal\.dump on the response/ do 60 | begin 61 | Marshal.dump(@response_from_httparty) 62 | rescue TypeError => e 63 | @exception_from_httparty = e 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /features/steps/mongrel_helper.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | class BasicMongrelHandler < Mongrel::HttpHandler 3 | attr_accessor :content_type, :custom_headers, :response_body, :response_code, :preprocessor, :username, :password 4 | 5 | def initialize 6 | @content_type = "text/html" 7 | @response_body = "" 8 | @response_code = 200 9 | @custom_headers = {} 10 | end 11 | 12 | def process(request, response) 13 | instance_eval(&preprocessor) if preprocessor 14 | reply_with(response, response_code, response_body) 15 | end 16 | 17 | def reply_with(response, code, response_body) 18 | response.start(code) do |head, body| 19 | head["Content-Type"] = content_type 20 | custom_headers.each { |k, v| head[k] = v } 21 | body.write(response_body) 22 | end 23 | end 24 | end 25 | 26 | class DeflateHandler < BasicMongrelHandler 27 | def process(request, response) 28 | accept_encoding = request.params["HTTP_ACCEPT_ENCODING"] 29 | if accept_encoding.nil? || !accept_encoding.include?('deflate') 30 | reply_with(response, 406, 'No deflate accept encoding found in request') 31 | else 32 | response.start do |head, body| 33 | require 'zlib' 34 | head['Content-Encoding'] = 'deflate' 35 | body.write Zlib::Deflate.deflate(response_body) 36 | end 37 | end 38 | end 39 | end 40 | 41 | class GzipHandler < BasicMongrelHandler 42 | def process(request, response) 43 | accept_encoding = request.params["HTTP_ACCEPT_ENCODING"] 44 | if accept_encoding.nil? || !accept_encoding.include?('gzip') 45 | reply_with(response, 406, 'No gzip accept encoding found in request') 46 | else 47 | response.start do |head, body| 48 | head['Content-Encoding'] = 'gzip' 49 | body.write gzip(response_body) 50 | end 51 | end 52 | end 53 | 54 | protected 55 | 56 | def gzip(string) 57 | require 'zlib' 58 | sio = StringIO.new('', 'r+') 59 | gz = Zlib::GzipWriter.new sio 60 | gz.write string 61 | gz.finish 62 | sio.rewind 63 | sio.read 64 | end 65 | end 66 | 67 | module BasicAuthentication 68 | def self.extended(base) 69 | base.custom_headers["WWW-Authenticate"] = 'Basic Realm="Super Secret Page"' 70 | end 71 | 72 | def process(request, response) 73 | if authorized?(request) 74 | super 75 | else 76 | reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") 77 | end 78 | end 79 | 80 | def authorized?(request) 81 | request.params["HTTP_AUTHORIZATION"] == "Basic " + Base64.encode64("#{@username}:#{@password}").strip 82 | end 83 | end 84 | 85 | module DigestAuthentication 86 | def self.extended(base) 87 | base.custom_headers["WWW-Authenticate"] = 'Digest realm="testrealm@host.com",qop="auth,auth-int",nonce="nonce",opaque="opaque"' 88 | end 89 | 90 | def process(request, response) 91 | if authorized?(request) 92 | super 93 | else 94 | reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") 95 | end 96 | end 97 | 98 | def authorized?(request) 99 | request.params["HTTP_AUTHORIZATION"] =~ /Digest.*uri=/ 100 | end 101 | end 102 | 103 | module DigestAuthenticationUsingMD5Sess 104 | NONCE = 'nonce' 105 | REALM = 'testrealm@host.com' 106 | QOP = 'auth,auth-int' 107 | def self.extended(base) 108 | base.custom_headers["WWW-Authenticate"] = %(Digest realm="#{REALM}",qop="#{QOP}",algorithm="MD5-sess",nonce="#{NONCE}",opaque="opaque"') 109 | end 110 | 111 | def process(request, response) 112 | if authorized?(request) 113 | super 114 | else 115 | reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") 116 | end 117 | end 118 | 119 | def md5(str) 120 | Digest::MD5.hexdigest(str) 121 | end 122 | 123 | def authorized?(request) 124 | auth = request.params["HTTP_AUTHORIZATION"] 125 | params = {} 126 | auth.to_s.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } 127 | a1a = [@username,REALM,@password].join(':') 128 | a1 = [md5(a1a),NONCE,params['cnonce'] ].join(':') 129 | a2 = [ request.params["REQUEST_METHOD"], request.params["REQUEST_URI"] ] .join(':') 130 | expected_response = md5( [md5(a1), NONCE, params['nc'], params['cnonce'], QOP, md5(a2)].join(':') ) 131 | expected_response == params['response'] 132 | end 133 | end 134 | 135 | 136 | def new_mongrel_redirector(target_url, relative_path = false) 137 | target_url = "http://#{@host_and_port}#{target_url}" unless relative_path 138 | Mongrel::RedirectHandler.new(target_url) 139 | end 140 | -------------------------------------------------------------------------------- /features/steps/remote_service_steps.rb: -------------------------------------------------------------------------------- 1 | Given /a remote service that returns '(.*)'/ do |response_body| 2 | @handler = BasicMongrelHandler.new 3 | step "the response from the service has a body of '#{response_body}'" 4 | end 5 | 6 | Given /^a remote service that returns:$/ do |response_body| 7 | @handler = BasicMongrelHandler.new 8 | @handler.response_body = response_body 9 | end 10 | 11 | Given /a remote service that returns a (\d+) status code/ do |code| 12 | @handler = BasicMongrelHandler.new 13 | @handler.response_code = code 14 | end 15 | 16 | Given /that service is accessed at the path '(.*)'/ do |path| 17 | @server.register(path, @handler) 18 | end 19 | 20 | Given /^that service takes (\d+) (.*) to generate a response$/ do |time, unit| 21 | time = time.to_i 22 | time *= 60 if unit =~ /minute/ 23 | @server_response_time = time 24 | @handler.preprocessor = proc { sleep time } 25 | end 26 | 27 | Given /^a remote deflate service$/ do 28 | @handler = DeflateHandler.new 29 | end 30 | 31 | Given /^a remote deflate service on port '(\d+)'/ do |port| 32 | run_server(port) 33 | @handler = DeflateHandler.new 34 | end 35 | 36 | Given /^a remote gzip service$/ do 37 | @handler = GzipHandler.new 38 | end 39 | 40 | Given /the response from the service has a Content-Type of '(.*)'/ do |content_type| 41 | @handler.content_type = content_type 42 | end 43 | 44 | Given /the response from the service has a body of '(.*)'/ do |response_body| 45 | @handler.response_body = response_body 46 | end 47 | 48 | Given /the url '(.*)' redirects to '(.*)'/ do |redirection_url, target_url| 49 | @server.register redirection_url, new_mongrel_redirector(target_url) 50 | end 51 | 52 | Given /that service is protected by Basic Authentication/ do 53 | @handler.extend BasicAuthentication 54 | end 55 | 56 | Given /that service is protected by Digest Authentication/ do 57 | @handler.extend DigestAuthentication 58 | end 59 | 60 | Given /that service is protected by MD5-sess Digest Authentication/ do 61 | @handler.extend DigestAuthenticationUsingMD5Sess 62 | end 63 | 64 | Given /that service requires the username '(.*)' with the password '(.*)'/ do |username, password| 65 | @handler.username = username 66 | @handler.password = password 67 | end 68 | 69 | # customize aruba cucumber step 70 | Then /^the output should contain '(.*)'$/ do |expected| 71 | expect(all_commands.map(&:output).join("\n")).to match_output_string(expected) 72 | end 73 | 74 | Given /a restricted page at '(.*)'/ do |url| 75 | steps " 76 | Given a remote service that returns 'A response I will never see' 77 | And that service is accessed at the path '#{url}' 78 | And that service is protected by Basic Authentication 79 | And that service requires the username 'something' with the password 'secret' 80 | " 81 | end 82 | 83 | # This joins the server thread, and halts cucumber, so you can actually hit the 84 | # server with a browser. Runs until you kill it with Ctrl-c 85 | Given /I want to hit this in a browser/ do 86 | @server.acceptor.join 87 | end 88 | 89 | Then /I wait for the server to recover/ do 90 | timeout = @request_options[:timeout] || 0 91 | sleep @server_response_time - timeout 92 | end 93 | -------------------------------------------------------------------------------- /features/supports_marshalling_with_logger_and_proc.feature: -------------------------------------------------------------------------------- 1 | Feature: Supports marshalling with request logger and/or proc parser 2 | In order to support caching responses 3 | As a developer 4 | I want the request to be able to be marshalled if I have set up a custom 5 | logger or have a proc as the response parser. 6 | 7 | Scenario: Marshal response with request logger 8 | Given a remote service that returns '{ "some": "data" }' 9 | And that service is accessed at the path '/somedata.json' 10 | When I set my HTTParty logger option 11 | And I call HTTParty#get with '/somedata.json' 12 | And I call Marshal.dump on the response 13 | Then it should not raise a TypeError exception 14 | 15 | Scenario: Marshal response with proc parser 16 | Given a remote service that returns '{ "some": "data" }' 17 | And that service is accessed at the path '/somedata.json' 18 | When I set my HTTParty parser option to a proc 19 | And I call HTTParty#get with '/somedata.json' 20 | And I call Marshal.dump on the response 21 | Then it should not raise a TypeError exception 22 | -------------------------------------------------------------------------------- /features/supports_read_timeout_option.feature: -------------------------------------------------------------------------------- 1 | Feature: Supports the read timeout option 2 | In order to handle inappropriately slow response times 3 | As a developer 4 | I want my request to raise an exception after my specified read_timeout as elapsed 5 | 6 | Scenario: A long running response 7 | Given a remote service that returns '

Some HTML

' 8 | And that service is accessed at the path '/long_running_service.html' 9 | And that service takes 2 seconds to generate a response 10 | When I set my HTTParty read_timeout option to 1 11 | And I call HTTParty#get with '/long_running_service.html' 12 | Then it should raise a Timeout::Error exception 13 | And I wait for the server to recover 14 | -------------------------------------------------------------------------------- /features/supports_redirection.feature: -------------------------------------------------------------------------------- 1 | Feature: Supports Redirection 2 | 3 | As a developer 4 | I want to work with services that may redirect me 5 | And I want it to follow a reasonable number of redirects 6 | Because sometimes web services do that 7 | 8 | Scenario: A service that redirects once 9 | Given a remote service that returns 'Service Response' 10 | And that service is accessed at the path '/landing_service.html' 11 | And the url '/redirector.html' redirects to '/landing_service.html' 12 | When I call HTTParty#get with '/redirector.html' 13 | Then the return value should match 'Service Response' 14 | 15 | # TODO: Look in to why this actually fails... 16 | Scenario: A service that redirects to a relative URL 17 | 18 | Scenario: A service that redirects infinitely 19 | Given the url '/first.html' redirects to '/second.html' 20 | And the url '/second.html' redirects to '/first.html' 21 | When I call HTTParty#get with '/first.html' 22 | Then it should raise an HTTParty::RedirectionTooDeep exception 23 | -------------------------------------------------------------------------------- /features/supports_timeout_option.feature: -------------------------------------------------------------------------------- 1 | Feature: Supports the timeout option 2 | In order to handle inappropriately slow response times 3 | As a developer 4 | I want my request to raise an exception after my specified timeout as elapsed 5 | 6 | Scenario: A long running response 7 | Given a remote service that returns '

Some HTML

' 8 | And that service is accessed at the path '/long_running_service.html' 9 | And that service takes 2 seconds to generate a response 10 | When I set my HTTParty timeout option to 1 11 | And I call HTTParty#get with '/long_running_service.html' 12 | Then it should raise a Timeout::Error exception 13 | And I wait for the server to recover 14 | -------------------------------------------------------------------------------- /httparty.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH.push File.expand_path("../lib", __FILE__) 3 | require "httparty/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "httparty" 7 | s.version = HTTParty::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.licenses = ['MIT'] 10 | s.authors = ["John Nunemaker", "Sandro Turriate"] 11 | s.email = ["nunemaker@gmail.com"] 12 | s.homepage = "https://github.com/jnunemaker/httparty" 13 | s.summary = 'Makes http fun! Also, makes consuming restful web services dead easy.' 14 | s.description = 'Makes http fun! Also, makes consuming restful web services dead easy.' 15 | 16 | s.required_ruby_version = '>= 2.7.0' 17 | 18 | s.add_dependency 'csv' 19 | s.add_dependency 'multi_xml', ">= 0.5.2" 20 | s.add_dependency 'mini_mime', ">= 1.0.0" 21 | 22 | # If this line is removed, all hard partying will cease. 23 | s.post_install_message = "When you HTTParty, you must party hard!" 24 | 25 | all_files = `git ls-files`.split("\n") 26 | test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 27 | 28 | s.files = all_files - test_files 29 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 30 | s.require_paths = ["lib"] 31 | end 32 | -------------------------------------------------------------------------------- /lib/httparty/connection_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | # Default connection adapter that returns a new Net::HTTP each time 5 | # 6 | # == Custom Connection Factories 7 | # 8 | # If you like to implement your own connection adapter, subclassing 9 | # HTTParty::ConnectionAdapter will make it easier. Just override 10 | # the #connection method. The uri and options attributes will have 11 | # all the info you need to construct your http connection. Whatever 12 | # you return from your connection method needs to adhere to the 13 | # Net::HTTP interface as this is what HTTParty expects. 14 | # 15 | # @example log the uri and options 16 | # class LoggingConnectionAdapter < HTTParty::ConnectionAdapter 17 | # def connection 18 | # puts uri 19 | # puts options 20 | # Net::HTTP.new(uri) 21 | # end 22 | # end 23 | # 24 | # @example count number of http calls 25 | # class CountingConnectionAdapter < HTTParty::ConnectionAdapter 26 | # @@count = 0 27 | # 28 | # self.count 29 | # @@count 30 | # end 31 | # 32 | # def connection 33 | # self.count += 1 34 | # super 35 | # end 36 | # end 37 | # 38 | # === Configuration 39 | # There is lots of configuration data available for your connection adapter 40 | # in the #options attribute. It is up to you to interpret them within your 41 | # connection adapter. Take a look at the implementation of 42 | # HTTParty::ConnectionAdapter#connection for examples of how they are used. 43 | # The keys used in options are 44 | # * :+timeout+: timeout in seconds 45 | # * :+open_timeout+: http connection open_timeout in seconds, overrides timeout if set 46 | # * :+read_timeout+: http connection read_timeout in seconds, overrides timeout if set 47 | # * :+write_timeout+: http connection write_timeout in seconds, overrides timeout if set (Ruby >= 2.6.0 required) 48 | # * :+debug_output+: see HTTParty::ClassMethods.debug_output. 49 | # * :+cert_store+: contains certificate data. see method 'attach_ssl_certificates' 50 | # * :+pem+: contains pem client certificate data. see method 'attach_ssl_certificates' 51 | # * :+p12+: contains PKCS12 client client certificate data. see method 'attach_ssl_certificates' 52 | # * :+verify+: verify the server’s certificate against the ca certificate. 53 | # * :+verify_peer+: set to false to turn off server verification but still send client certificate 54 | # * :+ssl_ca_file+: see HTTParty::ClassMethods.ssl_ca_file. 55 | # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path. 56 | # * :+ssl_version+: SSL versions to allow. see method 'attach_ssl_certificates' 57 | # * :+ciphers+: The list of SSL ciphers to support 58 | # * :+connection_adapter_options+: contains the hash you passed to HTTParty.connection_adapter when you configured your connection adapter 59 | # * :+local_host+: The local address to bind to 60 | # * :+local_port+: The local port to bind to 61 | # * :+http_proxyaddr+: HTTP Proxy address 62 | # * :+http_proxyport+: HTTP Proxy port 63 | # * :+http_proxyuser+: HTTP Proxy user 64 | # * :+http_proxypass+: HTTP Proxy password 65 | # 66 | # === Inherited methods 67 | # * :+clean_host+: Method used to sanitize host names 68 | 69 | class ConnectionAdapter 70 | # Private: Regex used to strip brackets from IPv6 URIs. 71 | StripIpv6BracketsRegex = /\A\[(.*)\]\z/ 72 | 73 | OPTION_DEFAULTS = { 74 | verify: true, 75 | verify_peer: true 76 | } 77 | 78 | # Public 79 | def self.call(uri, options) 80 | new(uri, options).connection 81 | end 82 | 83 | def self.default_cert_store 84 | @default_cert_store ||= OpenSSL::X509::Store.new.tap do |cert_store| 85 | cert_store.set_default_paths 86 | end 87 | end 88 | 89 | attr_reader :uri, :options 90 | 91 | def initialize(uri, options = {}) 92 | uri_adapter = options[:uri_adapter] || URI 93 | raise ArgumentError, "uri must be a #{uri_adapter}, not a #{uri.class}" unless uri.is_a? uri_adapter 94 | 95 | @uri = uri 96 | @options = OPTION_DEFAULTS.merge(options) 97 | end 98 | 99 | def connection 100 | host = clean_host(uri.host) 101 | port = uri.port || (uri.scheme == 'https' ? 443 : 80) 102 | if options.key?(:http_proxyaddr) 103 | http = Net::HTTP.new( 104 | host, 105 | port, 106 | options[:http_proxyaddr], 107 | options[:http_proxyport], 108 | options[:http_proxyuser], 109 | options[:http_proxypass] 110 | ) 111 | else 112 | http = Net::HTTP.new(host, port) 113 | end 114 | 115 | http.use_ssl = ssl_implied?(uri) 116 | 117 | attach_ssl_certificates(http, options) 118 | 119 | if add_timeout?(options[:timeout]) 120 | http.open_timeout = options[:timeout] 121 | http.read_timeout = options[:timeout] 122 | http.write_timeout = options[:timeout] 123 | end 124 | 125 | if add_timeout?(options[:read_timeout]) 126 | http.read_timeout = options[:read_timeout] 127 | end 128 | 129 | if add_timeout?(options[:open_timeout]) 130 | http.open_timeout = options[:open_timeout] 131 | end 132 | 133 | if add_timeout?(options[:write_timeout]) 134 | http.write_timeout = options[:write_timeout] 135 | end 136 | 137 | if add_max_retries?(options[:max_retries]) 138 | http.max_retries = options[:max_retries] 139 | end 140 | 141 | if options[:debug_output] 142 | http.set_debug_output(options[:debug_output]) 143 | end 144 | 145 | if options[:ciphers] 146 | http.ciphers = options[:ciphers] 147 | end 148 | 149 | # Bind to a specific local address or port 150 | # 151 | # @see https://bugs.ruby-lang.org/issues/6617 152 | if options[:local_host] 153 | http.local_host = options[:local_host] 154 | end 155 | 156 | if options[:local_port] 157 | http.local_port = options[:local_port] 158 | end 159 | 160 | http 161 | end 162 | 163 | private 164 | 165 | def add_timeout?(timeout) 166 | timeout && (timeout.is_a?(Integer) || timeout.is_a?(Float)) 167 | end 168 | 169 | def add_max_retries?(max_retries) 170 | max_retries && max_retries.is_a?(Integer) && max_retries >= 0 171 | end 172 | 173 | def clean_host(host) 174 | strip_ipv6_brackets(host) 175 | end 176 | 177 | def strip_ipv6_brackets(host) 178 | StripIpv6BracketsRegex =~ host ? $1 : host 179 | end 180 | 181 | def ssl_implied?(uri) 182 | uri.port == 443 || uri.scheme == 'https' 183 | end 184 | 185 | def verify_ssl_certificate? 186 | !(options[:verify] == false || options[:verify_peer] == false) 187 | end 188 | 189 | def attach_ssl_certificates(http, options) 190 | if http.use_ssl? 191 | if options.fetch(:verify, true) 192 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 193 | if options[:cert_store] 194 | http.cert_store = options[:cert_store] 195 | else 196 | # Use the default cert store by default, i.e. system ca certs 197 | http.cert_store = self.class.default_cert_store 198 | end 199 | else 200 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 201 | end 202 | 203 | # Client certificate authentication 204 | # Note: options[:pem] must contain the content of a PEM file having the private key appended 205 | if options[:pem] 206 | http.cert = OpenSSL::X509::Certificate.new(options[:pem]) 207 | http.key = OpenSSL::PKey.read(options[:pem], options[:pem_password]) 208 | http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE 209 | end 210 | 211 | # PKCS12 client certificate authentication 212 | if options[:p12] 213 | p12 = OpenSSL::PKCS12.new(options[:p12], options[:p12_password]) 214 | http.cert = p12.certificate 215 | http.key = p12.key 216 | http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE 217 | end 218 | 219 | # SSL certificate authority file and/or directory 220 | if options[:ssl_ca_file] 221 | http.ca_file = options[:ssl_ca_file] 222 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 223 | end 224 | 225 | if options[:ssl_ca_path] 226 | http.ca_path = options[:ssl_ca_path] 227 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 228 | end 229 | 230 | # This is only Ruby 1.9+ 231 | if options[:ssl_version] && http.respond_to?(:ssl_version=) 232 | http.ssl_version = options[:ssl_version] 233 | end 234 | end 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /lib/httparty/cookie_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HTTParty::CookieHash < Hash #:nodoc: 4 | CLIENT_COOKIES = %w(path expires domain path secure httponly samesite) 5 | 6 | def add_cookies(data) 7 | case data 8 | when Hash 9 | merge!(data) 10 | when String 11 | data.split('; ').each do |cookie| 12 | key, value = cookie.split('=', 2) 13 | self[key.to_sym] = value if key 14 | end 15 | else 16 | raise "add_cookies only takes a Hash or a String" 17 | end 18 | end 19 | 20 | def to_cookie_string 21 | select { |k, v| !CLIENT_COOKIES.include?(k.to_s.downcase) }.collect { |k, v| "#{k}=#{v}" }.join('; ') 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/httparty/decompressor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | # Decompresses the response body based on the Content-Encoding header. 5 | # 6 | # Net::HTTP automatically decompresses Content-Encoding values "gzip" and "deflate". 7 | # This class will handle "br" (Brotli) and "compress" (LZW) if the requisite 8 | # gems are installed. Otherwise, it returns nil if the body data cannot be 9 | # decompressed. 10 | # 11 | # @abstract Read the HTTP Compression section for more information. 12 | class Decompressor 13 | 14 | # "gzip" and "deflate" are handled by Net::HTTP 15 | # hence they do not need to be handled by HTTParty 16 | SupportedEncodings = { 17 | 'none' => :none, 18 | 'identity' => :none, 19 | 'br' => :brotli, 20 | 'compress' => :lzw, 21 | 'zstd' => :zstd 22 | }.freeze 23 | 24 | # The response body of the request 25 | # @return [String] 26 | attr_reader :body 27 | 28 | # The Content-Encoding algorithm used to encode the body 29 | # @return [Symbol] e.g. :gzip 30 | attr_reader :encoding 31 | 32 | # @param [String] body - the response body of the request 33 | # @param [Symbol] encoding - the Content-Encoding algorithm used to encode the body 34 | def initialize(body, encoding) 35 | @body = body 36 | @encoding = encoding 37 | end 38 | 39 | # Perform decompression on the response body 40 | # @return [String] the decompressed body 41 | # @return [nil] when the response body is nil or cannot decompressed 42 | def decompress 43 | return nil if body.nil? 44 | return body if encoding.nil? || encoding.strip.empty? 45 | 46 | if supports_encoding? 47 | decompress_supported_encoding 48 | else 49 | nil 50 | end 51 | end 52 | 53 | protected 54 | 55 | def supports_encoding? 56 | SupportedEncodings.keys.include?(encoding) 57 | end 58 | 59 | def decompress_supported_encoding 60 | method = SupportedEncodings[encoding] 61 | if respond_to?(method, true) 62 | send(method) 63 | else 64 | raise NotImplementedError, "#{self.class.name} has not implemented a decompression method for #{encoding.inspect} encoding." 65 | end 66 | end 67 | 68 | def none 69 | body 70 | end 71 | 72 | def brotli 73 | return nil unless defined?(::Brotli) 74 | begin 75 | ::Brotli.inflate(body) 76 | rescue StandardError 77 | nil 78 | end 79 | end 80 | 81 | def lzw 82 | begin 83 | if defined?(::LZWS::String) 84 | ::LZWS::String.decompress(body) 85 | elsif defined?(::LZW::Simple) 86 | ::LZW::Simple.new.decompress(body) 87 | end 88 | rescue StandardError 89 | nil 90 | end 91 | end 92 | 93 | def zstd 94 | return nil unless defined?(::Zstd) 95 | begin 96 | ::Zstd.decompress(body) 97 | rescue StandardError 98 | nil 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/httparty/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | COMMON_NETWORK_ERRORS = [ 5 | EOFError, 6 | Errno::ECONNABORTED, 7 | Errno::ECONNREFUSED, 8 | Errno::ECONNRESET, 9 | Errno::EHOSTUNREACH, 10 | Errno::EINVAL, 11 | Errno::ENETUNREACH, 12 | Errno::ENOTSOCK, 13 | Errno::EPIPE, 14 | Errno::ETIMEDOUT, 15 | Net::HTTPBadResponse, 16 | Net::HTTPHeaderSyntaxError, 17 | Net::ProtocolError, 18 | Net::ReadTimeout, 19 | OpenSSL::SSL::SSLError, 20 | SocketError, 21 | Timeout::Error # Also covers subclasses like Net::OpenTimeout 22 | ].freeze 23 | 24 | # @abstract Exceptions raised by HTTParty inherit from Error 25 | class Error < StandardError; end 26 | 27 | # @abstract Exceptions raised by HTTParty inherit from this because it is funny 28 | # and if you don't like fun you should be using a different library. 29 | class Foul < Error; end 30 | 31 | # Exception raised when you attempt to set a non-existent format 32 | class UnsupportedFormat < Foul; end 33 | 34 | # Exception raised when using a URI scheme other than HTTP or HTTPS 35 | class UnsupportedURIScheme < Foul; end 36 | 37 | # @abstract Exceptions which inherit from ResponseError contain the Net::HTTP 38 | # response object accessible via the {#response} method. 39 | class ResponseError < Foul 40 | # Returns the response of the last request 41 | # @return [Net::HTTPResponse] A subclass of Net::HTTPResponse, e.g. 42 | # Net::HTTPOK 43 | attr_reader :response 44 | 45 | # Instantiate an instance of ResponseError with a Net::HTTPResponse object 46 | # @param [Net::HTTPResponse] 47 | def initialize(response) 48 | @response = response 49 | super(response) 50 | end 51 | end 52 | 53 | # Exception that is raised when request has redirected too many times. 54 | # Calling {#response} returns the Net:HTTP response object. 55 | class RedirectionTooDeep < ResponseError; end 56 | 57 | # Exception that is raised when request redirects and location header is present more than once 58 | class DuplicateLocationHeader < ResponseError; end 59 | 60 | # Exception that is raised when common network errors occur. 61 | class NetworkError < Foul; end 62 | end 63 | -------------------------------------------------------------------------------- /lib/httparty/hash_conversions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | 5 | module HTTParty 6 | module HashConversions 7 | # @return This hash as a query string 8 | # 9 | # @example 10 | # { name: "Bob", 11 | # address: { 12 | # street: '111 Ruby Ave.', 13 | # city: 'Ruby Central', 14 | # phones: ['111-111-1111', '222-222-2222'] 15 | # } 16 | # }.to_params 17 | # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave." 18 | def self.to_params(hash) 19 | hash.to_hash.map { |k, v| normalize_param(k, v) }.join.chop 20 | end 21 | 22 | # @param key The key for the param. 23 | # @param value The value for the param. 24 | # 25 | # @return This key value pair as a param 26 | # 27 | # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&" 28 | def self.normalize_param(key, value) 29 | normalized_keys = normalize_keys(key, value) 30 | 31 | normalized_keys.flatten.each_slice(2).inject(''.dup) do |string, (k, v)| 32 | string << "#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v.to_s)}&" 33 | end 34 | end 35 | 36 | def self.normalize_keys(key, value) 37 | stack = [] 38 | normalized_keys = [] 39 | 40 | if value.respond_to?(:to_ary) 41 | if value.empty? 42 | normalized_keys << ["#{key}[]", ''] 43 | else 44 | normalized_keys = value.to_ary.flat_map do |element| 45 | normalize_keys("#{key}[]", element) 46 | end 47 | end 48 | elsif value.respond_to?(:to_hash) 49 | stack << [key, value.to_hash] 50 | else 51 | normalized_keys << [key.to_s, value] 52 | end 53 | 54 | stack.each do |parent, hash| 55 | hash.each do |child_key, child_value| 56 | if child_value.respond_to?(:to_hash) 57 | stack << ["#{parent}[#{child_key}]", child_value.to_hash] 58 | elsif child_value.respond_to?(:to_ary) 59 | child_value.to_ary.each do |v| 60 | normalized_keys << normalize_keys("#{parent}[#{child_key}][]", v).flatten 61 | end 62 | else 63 | normalized_keys << normalize_keys("#{parent}[#{child_key}]", child_value).flatten 64 | end 65 | end 66 | end 67 | 68 | normalized_keys 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/httparty/headers_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | class HeadersProcessor 5 | attr_reader :headers, :options 6 | 7 | def initialize(headers, options) 8 | @headers = headers 9 | @options = options 10 | end 11 | 12 | def call 13 | return unless options[:headers] 14 | 15 | options[:headers] = headers.merge(options[:headers]) if headers.any? 16 | options[:headers] = Utils.stringify_keys(process_dynamic_headers) 17 | end 18 | 19 | private 20 | 21 | def process_dynamic_headers 22 | options[:headers].each_with_object({}) do |header, processed_headers| 23 | key, value = header 24 | processed_headers[key] = if value.respond_to?(:call) 25 | value.arity == 0 ? value.call : value.call(options) 26 | else 27 | value 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/httparty/logger/apache_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | module Logger 5 | class ApacheFormatter #:nodoc: 6 | TAG_NAME = HTTParty.name 7 | 8 | attr_accessor :level, :logger 9 | 10 | def initialize(logger, level) 11 | @logger = logger 12 | @level = level.to_sym 13 | end 14 | 15 | def format(request, response) 16 | @request = request 17 | @response = response 18 | 19 | logger.public_send level, message 20 | end 21 | 22 | private 23 | 24 | attr_reader :request, :response 25 | 26 | def message 27 | "[#{TAG_NAME}] [#{current_time}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} " 28 | end 29 | 30 | def current_time 31 | Time.now.strftime('%Y-%m-%d %H:%M:%S %z') 32 | end 33 | 34 | def http_method 35 | request.http_method.name.split('::').last.upcase 36 | end 37 | 38 | def path 39 | request.path.to_s 40 | end 41 | 42 | def content_length 43 | response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length'] 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/httparty/logger/curl_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | module Logger 5 | class CurlFormatter #:nodoc: 6 | TAG_NAME = HTTParty.name 7 | OUT = '>' 8 | IN = '<' 9 | 10 | attr_accessor :level, :logger 11 | 12 | def initialize(logger, level) 13 | @logger = logger 14 | @level = level.to_sym 15 | @messages = [] 16 | end 17 | 18 | def format(request, response) 19 | @request = request 20 | @response = response 21 | 22 | log_request 23 | log_response 24 | 25 | logger.public_send level, messages.join("\n") 26 | end 27 | 28 | private 29 | 30 | attr_reader :request, :response 31 | attr_accessor :messages 32 | 33 | def log_request 34 | log_url 35 | log_headers 36 | log_query 37 | log OUT, request.raw_body if request.raw_body 38 | log OUT 39 | end 40 | 41 | def log_response 42 | log IN, "HTTP/#{response.http_version} #{response.code}" 43 | log_response_headers 44 | log IN, "\n#{response.body}" 45 | log IN 46 | end 47 | 48 | def log_url 49 | http_method = request.http_method.name.split('::').last.upcase 50 | uri = if request.options[:base_uri] 51 | request.options[:base_uri] + request.path.path 52 | else 53 | request.path.to_s 54 | end 55 | 56 | log OUT, "#{http_method} #{uri}" 57 | end 58 | 59 | def log_headers 60 | return unless request.options[:headers] && request.options[:headers].size > 0 61 | 62 | log OUT, 'Headers: ' 63 | log_hash request.options[:headers] 64 | end 65 | 66 | def log_query 67 | return unless request.options[:query] 68 | 69 | log OUT, 'Query: ' 70 | log_hash request.options[:query] 71 | end 72 | 73 | def log_response_headers 74 | headers = response.respond_to?(:headers) ? response.headers : response 75 | response.each_header do |response_header| 76 | log IN, "#{response_header.capitalize}: #{headers[response_header]}" 77 | end 78 | end 79 | 80 | def log_hash(hash) 81 | hash.each { |k, v| log(OUT, "#{k}: #{v}") } 82 | end 83 | 84 | def log(direction, line = '') 85 | messages << "[#{TAG_NAME}] [#{current_time}] #{direction} #{line}" 86 | end 87 | 88 | def current_time 89 | Time.now.strftime("%Y-%m-%d %H:%M:%S %z") 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/httparty/logger/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'httparty/logger/apache_formatter' 4 | require 'httparty/logger/curl_formatter' 5 | require 'httparty/logger/logstash_formatter' 6 | 7 | module HTTParty 8 | module Logger 9 | def self.formatters 10 | @formatters ||= { 11 | :curl => Logger::CurlFormatter, 12 | :apache => Logger::ApacheFormatter, 13 | :logstash => Logger::LogstashFormatter, 14 | } 15 | end 16 | 17 | def self.add_formatter(name, formatter) 18 | raise HTTParty::Error.new("Log Formatter with name #{name} already exists") if formatters.include?(name) 19 | formatters.merge!(name.to_sym => formatter) 20 | end 21 | 22 | def self.build(logger, level, formatter) 23 | level ||= :info 24 | formatter ||= :apache 25 | 26 | logger_klass = formatters[formatter] || Logger::ApacheFormatter 27 | logger_klass.new(logger, level) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/httparty/logger/logstash_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | module Logger 5 | class LogstashFormatter #:nodoc: 6 | TAG_NAME = HTTParty.name 7 | 8 | attr_accessor :level, :logger 9 | 10 | def initialize(logger, level) 11 | @logger = logger 12 | @level = level.to_sym 13 | end 14 | 15 | def format(request, response) 16 | @request = request 17 | @response = response 18 | 19 | logger.public_send level, logstash_message 20 | end 21 | 22 | private 23 | 24 | attr_reader :request, :response 25 | 26 | def logstash_message 27 | require 'json' 28 | { 29 | '@timestamp' => current_time, 30 | '@version' => 1, 31 | 'content_length' => content_length || '-', 32 | 'http_method' => http_method, 33 | 'message' => message, 34 | 'path' => path, 35 | 'response_code' => response.code, 36 | 'severity' => level, 37 | 'tags' => [TAG_NAME], 38 | }.to_json 39 | end 40 | 41 | def message 42 | "[#{TAG_NAME}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} " 43 | end 44 | 45 | def current_time 46 | Time.now.strftime('%Y-%m-%d %H:%M:%S %z') 47 | end 48 | 49 | def http_method 50 | @http_method ||= request.http_method.name.split('::').last.upcase 51 | end 52 | 53 | def path 54 | @path ||= request.path.to_s 55 | end 56 | 57 | def content_length 58 | @content_length ||= response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length'] 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/httparty/module_inheritable_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | module ModuleInheritableAttributes #:nodoc: 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | end 8 | 9 | # borrowed from Rails 3.2 ActiveSupport 10 | def self.hash_deep_dup(hash) 11 | duplicate = hash.dup 12 | 13 | duplicate.each_pair do |key, value| 14 | if value.is_a?(Hash) 15 | duplicate[key] = hash_deep_dup(value) 16 | elsif value.is_a?(Proc) 17 | duplicate[key] = value.dup 18 | else 19 | duplicate[key] = value 20 | end 21 | end 22 | 23 | duplicate 24 | end 25 | 26 | module ClassMethods #:nodoc: 27 | def mattr_inheritable(*args) 28 | @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs] 29 | @mattr_inheritable_attrs += args 30 | 31 | args.each do |arg| 32 | singleton_class.attr_accessor(arg) 33 | end 34 | 35 | @mattr_inheritable_attrs 36 | end 37 | 38 | def inherited(subclass) 39 | super 40 | @mattr_inheritable_attrs.each do |inheritable_attribute| 41 | ivar = :"@#{inheritable_attribute}" 42 | subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone) 43 | 44 | if instance_variable_get(ivar).respond_to?(:merge) 45 | subclass.class_eval <<~RUBY, __FILE__, __LINE__ + 1 46 | def self.#{inheritable_attribute} 47 | duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar}) 48 | #{ivar} = superclass.#{inheritable_attribute}.merge(duplicate) 49 | end 50 | RUBY 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/httparty/net_digest_auth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'digest/md5' 4 | require 'net/http' 5 | 6 | module Net 7 | module HTTPHeader 8 | def digest_auth(username, password, response) 9 | authenticator = DigestAuthenticator.new( 10 | username, 11 | password, 12 | @method, 13 | @path, 14 | response 15 | ) 16 | 17 | authenticator.authorization_header.each do |v| 18 | add_field('Authorization', v) 19 | end 20 | 21 | authenticator.cookie_header.each do |v| 22 | add_field('Cookie', v) 23 | end 24 | end 25 | 26 | class DigestAuthenticator 27 | def initialize(username, password, method, path, response_header) 28 | @username = username 29 | @password = password 30 | @method = method 31 | @path = path 32 | @response = parse(response_header) 33 | @cookies = parse_cookies(response_header) 34 | end 35 | 36 | def authorization_header 37 | @cnonce = md5(random) 38 | header = [ 39 | %(Digest username="#{@username}"), 40 | %(realm="#{@response['realm']}"), 41 | %(nonce="#{@response['nonce']}"), 42 | %(uri="#{@path}"), 43 | %(response="#{request_digest}") 44 | ] 45 | 46 | header << %(algorithm="#{@response['algorithm']}") if algorithm_present? 47 | 48 | if qop_present? 49 | header << %(cnonce="#{@cnonce}") 50 | header << %(qop="#{@response['qop']}") 51 | header << 'nc=00000001' 52 | end 53 | 54 | header << %(opaque="#{@response['opaque']}") if opaque_present? 55 | header 56 | end 57 | 58 | def cookie_header 59 | @cookies 60 | end 61 | 62 | private 63 | 64 | def parse(response_header) 65 | header = response_header['www-authenticate'] 66 | 67 | header = header.gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"') 68 | 69 | header =~ /Digest (.*)/ 70 | params = {} 71 | if $1 72 | non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 } 73 | non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } 74 | end 75 | params 76 | end 77 | 78 | def parse_cookies(response_header) 79 | return [] unless response_header['Set-Cookie'] 80 | 81 | cookies = response_header['Set-Cookie'].split('; ') 82 | 83 | cookies.reduce([]) do |ret, cookie| 84 | ret << cookie 85 | ret 86 | end 87 | 88 | cookies 89 | end 90 | 91 | def opaque_present? 92 | @response.key?('opaque') && !@response['opaque'].empty? 93 | end 94 | 95 | def qop_present? 96 | @response.key?('qop') && !@response['qop'].empty? 97 | end 98 | 99 | def random 100 | format '%x', (Time.now.to_i + rand(65535)) 101 | end 102 | 103 | def request_digest 104 | a = [md5(a1), @response['nonce'], md5(a2)] 105 | a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present? 106 | md5(a.join(':')) 107 | end 108 | 109 | def md5(str) 110 | Digest::MD5.hexdigest(str) 111 | end 112 | 113 | def algorithm_present? 114 | @response.key?('algorithm') && !@response['algorithm'].empty? 115 | end 116 | 117 | def use_md5_sess? 118 | algorithm_present? && @response['algorithm'] == 'MD5-sess' 119 | end 120 | 121 | def a1 122 | a1_user_realm_pwd = [@username, @response['realm'], @password].join(':') 123 | if use_md5_sess? 124 | [ md5(a1_user_realm_pwd), @response['nonce'], @cnonce ].join(':') 125 | else 126 | a1_user_realm_pwd 127 | end 128 | end 129 | 130 | def a2 131 | [@method, @path].join(':') 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/httparty/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | # The default parser used by HTTParty, supports xml, json, html, csv and 5 | # plain text. 6 | # 7 | # == Custom Parsers 8 | # 9 | # If you'd like to do your own custom parsing, subclassing HTTParty::Parser 10 | # will make that process much easier. There are a few different ways you can 11 | # utilize HTTParty::Parser as a superclass. 12 | # 13 | # @example Intercept the parsing for all formats 14 | # class SimpleParser < HTTParty::Parser 15 | # def parse 16 | # perform_parsing 17 | # end 18 | # end 19 | # 20 | # @example Add the atom format and parsing method to the default parser 21 | # class AtomParsingIncluded < HTTParty::Parser 22 | # SupportedFormats.merge!( 23 | # {"application/atom+xml" => :atom} 24 | # ) 25 | # 26 | # def atom 27 | # perform_atom_parsing 28 | # end 29 | # end 30 | # 31 | # @example Only support the atom format 32 | # class ParseOnlyAtom < HTTParty::Parser 33 | # SupportedFormats = {"application/atom+xml" => :atom} 34 | # 35 | # def atom 36 | # perform_atom_parsing 37 | # end 38 | # end 39 | # 40 | # @abstract Read the Custom Parsers section for more information. 41 | class Parser 42 | SupportedFormats = { 43 | 'text/xml' => :xml, 44 | 'application/xml' => :xml, 45 | 'application/json' => :json, 46 | 'application/vnd.api+json' => :json, 47 | 'application/hal+json' => :json, 48 | 'text/json' => :json, 49 | 'application/javascript' => :plain, 50 | 'text/javascript' => :plain, 51 | 'text/html' => :html, 52 | 'text/plain' => :plain, 53 | 'text/csv' => :csv, 54 | 'application/csv' => :csv, 55 | 'text/comma-separated-values' => :csv 56 | } 57 | 58 | # The response body of the request 59 | # @return [String] 60 | attr_reader :body 61 | 62 | # The intended parsing format for the request 63 | # @return [Symbol] e.g. :json 64 | attr_reader :format 65 | 66 | # Instantiate the parser and call {#parse}. 67 | # @param [String] body the response body 68 | # @param [Symbol] format the response format 69 | # @return parsed response 70 | def self.call(body, format) 71 | new(body, format).parse 72 | end 73 | 74 | # @return [Hash] the SupportedFormats hash 75 | def self.formats 76 | const_get(:SupportedFormats) 77 | end 78 | 79 | # @param [String] mimetype response MIME type 80 | # @return [Symbol] 81 | # @return [nil] mime type not supported 82 | def self.format_from_mimetype(mimetype) 83 | formats[formats.keys.detect {|k| mimetype.include?(k)}] 84 | end 85 | 86 | # @return [Array] list of supported formats 87 | def self.supported_formats 88 | formats.values.uniq 89 | end 90 | 91 | # @param [Symbol] format e.g. :json, :xml 92 | # @return [Boolean] 93 | def self.supports_format?(format) 94 | supported_formats.include?(format) 95 | end 96 | 97 | def initialize(body, format) 98 | @body = body 99 | @format = format 100 | end 101 | 102 | # @return [Object] the parsed body 103 | # @return [nil] when the response body is nil, an empty string, spaces only or "null" 104 | def parse 105 | return nil if body.nil? 106 | return nil if body == 'null' 107 | return nil if body.valid_encoding? && body.strip.empty? 108 | if body.valid_encoding? && body.encoding == Encoding::UTF_8 109 | @body = body.gsub(/\A#{UTF8_BOM}/, '') 110 | end 111 | if supports_format? 112 | parse_supported_format 113 | else 114 | body 115 | end 116 | end 117 | 118 | protected 119 | 120 | def xml 121 | require 'multi_xml' 122 | MultiXml.parse(body) 123 | end 124 | 125 | UTF8_BOM = "\xEF\xBB\xBF" 126 | 127 | def json 128 | require 'json' 129 | JSON.parse(body, :quirks_mode => true, :allow_nan => true) 130 | end 131 | 132 | def csv 133 | require 'csv' 134 | CSV.parse(body) 135 | end 136 | 137 | def html 138 | body 139 | end 140 | 141 | def plain 142 | body 143 | end 144 | 145 | def supports_format? 146 | self.class.supports_format?(format) 147 | end 148 | 149 | def parse_supported_format 150 | if respond_to?(format, true) 151 | send(format) 152 | else 153 | raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format." 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/httparty/request/body.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'multipart_boundary' 4 | 5 | module HTTParty 6 | class Request 7 | class Body 8 | NEWLINE = "\r\n" 9 | private_constant :NEWLINE 10 | 11 | def initialize(params, query_string_normalizer: nil, force_multipart: false) 12 | @params = params 13 | @query_string_normalizer = query_string_normalizer 14 | @force_multipart = force_multipart 15 | end 16 | 17 | def call 18 | if params.respond_to?(:to_hash) 19 | multipart? ? generate_multipart : normalize_query(params) 20 | else 21 | params 22 | end 23 | end 24 | 25 | def boundary 26 | @boundary ||= MultipartBoundary.generate 27 | end 28 | 29 | def multipart? 30 | params.respond_to?(:to_hash) && (force_multipart || has_file?(params)) 31 | end 32 | 33 | private 34 | 35 | # https://html.spec.whatwg.org/#multipart-form-data 36 | MULTIPART_FORM_DATA_REPLACEMENT_TABLE = { 37 | '"' => '%22', 38 | "\r" => '%0D', 39 | "\n" => '%0A' 40 | }.freeze 41 | 42 | def generate_multipart 43 | normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) } 44 | 45 | multipart = normalized_params.inject(''.dup) do |memo, (key, value)| 46 | memo << "--#{boundary}#{NEWLINE}" 47 | memo << %(Content-Disposition: form-data; name="#{key}") 48 | # value.path is used to support ActionDispatch::Http::UploadedFile 49 | # https://github.com/jnunemaker/httparty/pull/585 50 | memo << %(; filename="#{file_name(value).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)}") if file?(value) 51 | memo << NEWLINE 52 | memo << "Content-Type: #{content_type(value)}#{NEWLINE}" if file?(value) 53 | memo << NEWLINE 54 | memo << content_body(value) 55 | memo << NEWLINE 56 | end 57 | 58 | multipart << "--#{boundary}--#{NEWLINE}" 59 | end 60 | 61 | def has_file?(value) 62 | if value.respond_to?(:to_hash) 63 | value.to_hash.any? { |_, v| has_file?(v) } 64 | elsif value.respond_to?(:to_ary) 65 | value.to_ary.any? { |v| has_file?(v) } 66 | else 67 | file?(value) 68 | end 69 | end 70 | 71 | def file?(object) 72 | object.respond_to?(:path) && object.respond_to?(:read) 73 | end 74 | 75 | def normalize_query(query) 76 | if query_string_normalizer 77 | query_string_normalizer.call(query) 78 | else 79 | HashConversions.to_params(query) 80 | end 81 | end 82 | 83 | def content_body(object) 84 | if file?(object) 85 | object = (file = object).read 86 | file.rewind if file.respond_to?(:rewind) 87 | end 88 | 89 | object.to_s 90 | end 91 | 92 | def content_type(object) 93 | return object.content_type if object.respond_to?(:content_type) 94 | require 'mini_mime' 95 | mime = MiniMime.lookup_by_filename(object.path) 96 | mime ? mime.content_type : 'application/octet-stream' 97 | end 98 | 99 | def file_name(object) 100 | object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path) 101 | end 102 | 103 | attr_reader :params, :query_string_normalizer, :force_multipart 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/httparty/request/multipart_boundary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'securerandom' 4 | 5 | module HTTParty 6 | class Request 7 | class MultipartBoundary 8 | def self.generate 9 | "------------------------#{SecureRandom.urlsafe_base64(12)}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/httparty/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | class Response < Object 5 | def self.underscore(string) 6 | string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase 7 | end 8 | 9 | def self._load(data) 10 | req, resp, parsed_resp, resp_body = Marshal.load(data) 11 | 12 | new(req, resp, -> { parsed_resp }, body: resp_body) 13 | end 14 | 15 | attr_reader :request, :response, :body, :headers 16 | 17 | def initialize(request, response, parsed_block, options = {}) 18 | @request = request 19 | @response = response 20 | @body = options[:body] || response.body 21 | @parsed_block = parsed_block 22 | @headers = Headers.new(response.to_hash) 23 | 24 | if request.options[:logger] 25 | logger = ::HTTParty::Logger.build( 26 | request.options[:logger], 27 | request.options[:log_level], 28 | request.options[:log_format] 29 | ) 30 | logger.format(request, self) 31 | end 32 | 33 | throw_exception 34 | end 35 | 36 | def parsed_response 37 | @parsed_response ||= @parsed_block.call 38 | end 39 | 40 | def code 41 | response.code.to_i 42 | end 43 | 44 | def http_version 45 | response.http_version 46 | end 47 | 48 | def tap 49 | yield self 50 | self 51 | end 52 | 53 | def inspect 54 | inspect_id = ::Kernel::format '%x', (object_id * 2) 55 | %(#<#{self.class}:0x#{inspect_id} parsed_response=#{parsed_response.inspect}, @response=#{response.inspect}, @headers=#{headers.inspect}>) 56 | end 57 | 58 | CODES_TO_OBJ = ::Net::HTTPResponse::CODE_CLASS_TO_OBJ.merge ::Net::HTTPResponse::CODE_TO_OBJ 59 | 60 | CODES_TO_OBJ.each do |response_code, klass| 61 | name = klass.name.sub('Net::HTTP', '') 62 | name = "#{underscore(name)}?".to_sym 63 | 64 | define_method(name) do 65 | klass === response 66 | end 67 | end 68 | 69 | # Support old multiple_choice? method from pre 2.0.0 era. 70 | if ::RUBY_PLATFORM != 'java' 71 | alias_method :multiple_choice?, :multiple_choices? 72 | end 73 | 74 | # Support old status codes method from pre 2.6.0 era. 75 | if ::RUBY_PLATFORM != 'java' 76 | alias_method :gateway_time_out?, :gateway_timeout? 77 | alias_method :request_entity_too_large?, :payload_too_large? 78 | alias_method :request_time_out?, :request_timeout? 79 | alias_method :request_uri_too_long?, :uri_too_long? 80 | alias_method :requested_range_not_satisfiable?, :range_not_satisfiable? 81 | end 82 | 83 | def nil? 84 | warn_about_nil_deprecation 85 | response.nil? || response.body.nil? || response.body.empty? 86 | end 87 | 88 | def to_s 89 | if !response.nil? && !response.body.nil? && response.body.respond_to?(:to_s) 90 | response.body.to_s 91 | else 92 | inspect 93 | end 94 | end 95 | 96 | def pretty_print(pp) 97 | if !parsed_response.nil? && parsed_response.respond_to?(:pretty_print) 98 | parsed_response.pretty_print(pp) 99 | else 100 | super 101 | end 102 | end 103 | 104 | def display(port=$>) 105 | if !parsed_response.nil? && parsed_response.respond_to?(:display) 106 | parsed_response.display(port) 107 | elsif !response.nil? && !response.body.nil? && response.body.respond_to?(:display) 108 | response.body.display(port) 109 | else 110 | port.write(inspect) 111 | end 112 | end 113 | 114 | def respond_to_missing?(name, *args) 115 | return true if super 116 | parsed_response.respond_to?(name) || response.respond_to?(name) 117 | end 118 | 119 | def _dump(_level) 120 | Marshal.dump([request, response, parsed_response, body]) 121 | end 122 | 123 | protected 124 | 125 | def method_missing(name, *args, &block) 126 | if parsed_response.respond_to?(name) 127 | parsed_response.send(name, *args, &block) 128 | elsif response.respond_to?(name) 129 | response.send(name, *args, &block) 130 | else 131 | super 132 | end 133 | end 134 | 135 | def throw_exception 136 | if @request.options[:raise_on].to_a.detect { |c| code.to_s.match(/#{c.to_s}/) } 137 | ::Kernel.raise ::HTTParty::ResponseError.new(@response), "Code #{code} - #{body}" 138 | end 139 | end 140 | 141 | private 142 | 143 | def warn_about_nil_deprecation 144 | trace_line = caller.reject { |line| line.include?('httparty') }.first 145 | warning = "[DEPRECATION] HTTParty will no longer override `response#nil?`. " \ 146 | "This functionality will be removed in future versions. " \ 147 | "Please, add explicit check `response.body.nil? || response.body.empty?`. " \ 148 | "For more info refer to: https://github.com/jnunemaker/httparty/issues/568\n" \ 149 | "#{trace_line}" 150 | 151 | warn(warning) 152 | end 153 | end 154 | end 155 | 156 | require 'httparty/response/headers' 157 | -------------------------------------------------------------------------------- /lib/httparty/response/headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'delegate' 4 | 5 | module HTTParty 6 | class Response #:nodoc: 7 | class Headers < ::SimpleDelegator 8 | include ::Net::HTTPHeader 9 | 10 | def initialize(header_values = nil) 11 | @header = {} 12 | if header_values 13 | header_values.each_pair do |k,v| 14 | if v.is_a?(Array) 15 | v.each do |sub_v| 16 | add_field(k, sub_v) 17 | end 18 | else 19 | add_field(k, v) 20 | end 21 | end 22 | end 23 | super(@header) 24 | end 25 | 26 | def ==(other) 27 | if other.is_a?(::Net::HTTPHeader) 28 | @header == other.instance_variable_get(:@header) 29 | elsif other.is_a?(Hash) 30 | @header == other || @header == Headers.new(other).instance_variable_get(:@header) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/httparty/response_fragment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'delegate' 4 | 5 | module HTTParty 6 | # Allow access to http_response and code by delegation on fragment 7 | class ResponseFragment < SimpleDelegator 8 | attr_reader :http_response, :connection 9 | 10 | def code 11 | @http_response.code.to_i 12 | end 13 | 14 | def initialize(fragment, http_response, connection) 15 | @fragment = fragment 16 | @http_response = http_response 17 | @connection = connection 18 | super fragment 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/httparty/text_encoder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | class TextEncoder 5 | attr_reader :text, :content_type, :assume_utf16_is_big_endian 6 | 7 | def initialize(text, assume_utf16_is_big_endian: true, content_type: nil) 8 | @text = +text 9 | @content_type = content_type 10 | @assume_utf16_is_big_endian = assume_utf16_is_big_endian 11 | end 12 | 13 | def call 14 | if can_encode? 15 | encoded_text 16 | else 17 | text 18 | end 19 | end 20 | 21 | private 22 | 23 | def can_encode? 24 | ''.respond_to?(:encoding) && charset 25 | end 26 | 27 | def encoded_text 28 | if 'utf-16'.casecmp(charset) == 0 29 | encode_utf_16 30 | else 31 | encode_with_ruby_encoding 32 | end 33 | end 34 | 35 | def encode_utf_16 36 | if text.bytesize >= 2 37 | if text.getbyte(0) == 0xFF && text.getbyte(1) == 0xFE 38 | return text.force_encoding('UTF-16LE') 39 | elsif text.getbyte(0) == 0xFE && text.getbyte(1) == 0xFF 40 | return text.force_encoding('UTF-16BE') 41 | end 42 | end 43 | 44 | if assume_utf16_is_big_endian # option 45 | text.force_encoding('UTF-16BE') 46 | else 47 | text.force_encoding('UTF-16LE') 48 | end 49 | end 50 | 51 | def encode_with_ruby_encoding 52 | # NOTE: This will raise an argument error if the 53 | # charset does not exist 54 | encoding = Encoding.find(charset) 55 | text.force_encoding(encoding.to_s) 56 | rescue ArgumentError 57 | text 58 | end 59 | 60 | def charset 61 | return nil if content_type.nil? 62 | 63 | if (matchdata = content_type.match(/;\s*charset\s*=\s*([^=,;"\s]+)/i)) 64 | return matchdata.captures.first 65 | end 66 | 67 | if (matchdata = content_type.match(/;\s*charset\s*=\s*"((\\.|[^\\"])+)"/i)) 68 | return matchdata.captures.first.gsub(/\\(.)/, '\1') 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/httparty/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | module Utils 5 | def self.stringify_keys(hash) 6 | return hash.transform_keys(&:to_s) if hash.respond_to?(:transform_keys) 7 | 8 | hash.each_with_object({}) do |(key, value), new_hash| 9 | new_hash[key.to_s] = value 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/httparty/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HTTParty 4 | VERSION = '0.23.1' 5 | end 6 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: release 3 | #/ 4 | #/ Tag the version in the repo and push the gem. 5 | #/ 6 | 7 | set -e 8 | cd $(dirname "$0")/.. 9 | 10 | [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && { 11 | grep '^#/' <"$0"| cut -c4- 12 | exit 0 13 | } 14 | 15 | gem_name=httparty 16 | 17 | # Build a new gem archive. 18 | rm -rf $gem_name-*.gem 19 | gem build -q $gem_name.gemspec 20 | 21 | # Make sure we're on the main branch. 22 | (git branch | grep -q '* main') || { 23 | echo "Only release from the main branch." 24 | exit 1 25 | } 26 | 27 | # Figure out what version we're releasing. 28 | tag=v`ls $gem_name-*.gem | sed "s/^$gem_name-\(.*\)\.gem$/\1/"` 29 | 30 | echo "Releasing $tag" 31 | 32 | # Make sure we haven't released this version before. 33 | git fetch -t origin 34 | 35 | (git tag -l | grep -q "$tag") && { 36 | echo "Whoops, there's already a '${tag}' tag." 37 | exit 1 38 | } 39 | 40 | # Tag it and bag it. 41 | gem push $gem_name-*.gem && git tag "$tag" && 42 | git push origin main && git push origin "$tag" 43 | -------------------------------------------------------------------------------- /spec/fixtures/delicious.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/fixtures/empty.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnunemaker/httparty/9417ce158d732fecc753c656accb59c470cfb6f8/spec/fixtures/empty.xml -------------------------------------------------------------------------------- /spec/fixtures/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example HTML 5 | 6 | 7 | 8 |

Example

9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -d "generated" ] ; then 5 | echo >&2 "error: 'generated' directory already exists. Delete it first." 6 | exit 1 7 | fi 8 | 9 | mkdir generated 10 | 11 | # Generate the CA private key and certificate 12 | openssl req -batch -subj '/CN=INSECURE Test Certificate Authority' -newkey rsa:4096 -new -x509 -days 999999 -keyout generated/ca.key -nodes -out generated/ca.crt 13 | 14 | # Create symlinks for ssl_ca_path 15 | openssl generated 16 | 17 | # Generate the server private key and self-signed certificate 18 | openssl req -batch -subj '/CN=localhost' -newkey rsa:4096 -new -x509 -days 999999 -keyout generated/server.key -nodes -out generated/selfsigned.crt 19 | 20 | # Generate certificate signing request with bogus hostname 21 | openssl req -batch -subj '/CN=bogo' -new -key generated/server.key -nodes -out generated/bogushost.csr 22 | 23 | # Sign the certificate requests 24 | openssl x509 -CA generated/ca.crt -CAkey generated/ca.key -set_serial 1 -in generated/selfsigned.crt -out generated/server.crt -clrext -extfile openssl-exts.cnf -extensions cert -days 999999 25 | openssl x509 -req -CA generated/ca.crt -CAkey generated/ca.key -set_serial 1 -in generated/bogushost.csr -out generated/bogushost.crt -clrext -extfile openssl-exts.cnf -extensions cert -days 999999 26 | 27 | # Remove certificate signing requests 28 | rm -f generated/*.csr 29 | 30 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/bogushost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDDCNJTlNF 3 | Q1VSRSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTAgFw0xOTAzMjAyMDI5NTFa 4 | GA80NzU3MDIxMzIwMjk1MVowDzENMAsGA1UEAwwEYm9nbzCCAiIwDQYJKoZIhvcN 5 | AQEBBQADggIPADCCAgoCggIBALavqYkWGfPALQaE23pt55NY/7zGeM7PTA7UthnI 6 | NjLdXU8wHETAJYCLnE8IXLPZVNUfTIi05Y80IdNIXhdK2ZoC+OAqefXCxA/QCBRc 7 | ocFkQVLN0Upv9x28dI/h49/mXtg//zxfkD5bbOuSy6dlTjvPdn6AfiVhTqIhGszB 8 | DKMOLL4NLfq8LS7Usj0KZipscWlj20j5SbwWFKqzzkxm9Kr5qvWH3wFlqSx5e5bz 9 | tH+CEn+Jem1D3tbru2W0uS/ty59xQZJ3Ga+WfrPKSmqUmEj1GveZIbIjkmlnpXT3 10 | vgb6HUSNRglmGQ1SDOOj3qLfoQG9T4rvMS1m4Dco8araEEZJsNZpqouvA+dSluRj 11 | gpfz7BwtsfgF7zenYCtp/QSOU+wVBI8rwM9LE65PMWtehLo2aVUbRZZ55gwmlebO 12 | 2GB6A+ZQk7k2Bvp3U8ob4MZzbjT96aa4uVm8LoaYF9jV4gyfKZ4CB7FC/aYkOhzZ 13 | X6dkV2Y98DfixK5ewmEoKs44q5PQdRwQSp3qMISPu9TaE/2ylRjcBi2FO2dtDnHq 14 | kY+944D1x9OujeFG5qak7s+AlU2bGRwoYjRo9es6vGlIfws1ttkLOsU8HGizMXkt 15 | sZKDja+lS1vnYf790MUS4lI0xZSL3IVs+ihwDJ4/74sf6nZEmerd5S8leZQ5zyaP 16 | BpoNAgMBAAGjUDBOMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFH5EfRhO2HNbUhsB 17 | WRM+F5O1dkYbMB8GA1UdIwQYMBaAFALj11Spg/cQ5epRzUB+cNSHRZ4sMA0GCSqG 18 | SIb3DQEBCwUAA4ICAQDKZ6ifsFWV4xk9uC8vPpP4+Tn1FT1fGIqlfPmUFGxzhdR3 19 | 94p8MAiM2CR93zBJJAwMC8lPYjpDUACGzfGAmt5hIKs72c1E5zvOxw+54EieoKe9 20 | 1isKhJMTtfVqUsga7foZj8XoBv8VscFZOWBFc96vfkwg5vXkl/v8qk227ILKs2U+ 21 | Unp1iWT4uY5zofZvwFYKJd3JMAIQ7bkzuOtHJ2r7sCYoHG3B5aVPsA8uNbYC61ri 22 | 5PjAl5eSrluy5tnnbhJCrjtwJI/LF1Tq+le2MEJRt7z/n3rfbvcAlmM+4XOAWAKn 23 | fM5nbT5qfct65HIBaDjX2Kdd4eOSzUD8/XpPmUXyj7ZlQf0WdKKA1QoSLvOdhHW8 24 | sZlWbFBwfXEU199Zd3EF6f3N3yLIAnAGjs8JEITHFma9TRJDXMF4/2Chg5/iI2f9 25 | sQLVvv2zomstVCoOXFsDmVQmdxVO69wW2KFvucFT8DiJIRnL5x1ZIoG5aylHFdaV 26 | LAi6lIluj7ETZwP6tKKOzLHipx7Jr/CafpIcYBzgjODoBgW246v9P2VJtIi7mHus 27 | Cw4HkEzJSm4CZm36OIcMayvFYz0FBts61/GvXGFnglCWRvFZ+n795hoUM4XAuXZA 28 | 0fxMEH/ZCbILy63kqjGaaOaTk2+Yu2DSRccgKVH7IuxV9xE7AgO0JNTGCS5HNg== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFPzCCAyegAwIBAgIUIV37QRfu4LcX78hw64FG5rAnZG8wDQYJKoZIhvcNAQEL 3 | BQAwLjEsMCoGA1UEAwwjSU5TRUNVUkUgVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3Jp 4 | dHkwIBcNMTkwMzIwMjAyOTI4WhgPNDc1NzAyMTMyMDI5MjhaMC4xLDAqBgNVBAMM 5 | I0lOU0VDVVJFIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MIICIjANBgkqhkiG 6 | 9w0BAQEFAAOCAg8AMIICCgKCAgEA/LOBASLxwrE1i3lhpS3yigbdcSZIoyEBqIF/ 7 | yMuWqVKr/l0CDPwlM/iwF0/sToBbFtx0bkKCq7ztURgw1ItYUar+mP+DL8Ku0Fxz 8 | ZVWL3EM1E2vduqJlvbpE6F5I2OU0UC0enSX5xg34vkHSQVGM2VJXNBM6a+0aYxeP 9 | o2KjfFOXnsFNVvlMpg2GNNuHwaUXoOhd26pp35QDsyyAPVtiRfU3idB+Pg+Hp3mI 10 | FJLHZyRXpVmw3hNlhl+Iga/H//x5n99Scr1CQyl8Z8EE5QKpPa9znjsPJNifSFud 11 | a1rBGRDizMfPfn+imW5zXSSFf/2tJnpzP9kaZT5hwXe4HLZbHaoYssOJdXBYDfx9 12 | qdw4yrBKt14z5ZFDjCu7SelDnLtX1aZ/+V2RrkNHge/G54MrKdnAg2CbxeWdrir2 13 | OxlCngzeU0aDuNJczH83054Em/uu3a7xJipuDDOYZe6HTSXHNJgIUtmZhvu07PlO 14 | 3tBLY/VxMhgzUvCVYCH05VoZgkmy21TRFu8mNn/+7HiHt5QGTKEqGdlFGJ4gcPFH 15 | bq2/dNb45hPr6d1iNC4UM5ERTMpU2rvNQQhQwPiS2fzXhE8BwLxfB94T6hnJpDhT 16 | u8tExckzRLfIgvexorSQFvYj2Lj5YjPdIHJq/e/XVSkp6h6H93T7EHycLlTP5HZ2 17 | le6+d4ECAwEAAaNTMFEwHQYDVR0OBBYEFALj11Spg/cQ5epRzUB+cNSHRZ4sMB8G 18 | A1UdIwQYMBaAFALj11Spg/cQ5epRzUB+cNSHRZ4sMA8GA1UdEwEB/wQFMAMBAf8w 19 | DQYJKoZIhvcNAQELBQADggIBAG5PduOpr9ZZbR+tcyQUR1kxoc+vZMYQEi8fQyv9 20 | G8sL+/DRXj8fD0S/4aK+rSXuFMbQpL1njLIUxE2z2cCn6zOsXx1IY9rN5ryLSJad 21 | L4QXrPokNBi+HEGba7NvY98swD8PblUyeJqfi59pFj7nz4EcVhltVvgbYBMZUIPc 22 | IZBhyB7l9QaX5OZMGO1rJy8jxg8ng0vAliz+HQQavu8rNito90vpGvlXr1NQs11H 23 | SO4+lQMb8uuU4wqto+74gkLUNLsnAfAFWpIEV2JG+0SnVVbI2TFjvFiu7P/TTGxQ 24 | MRrzEVQAXRSIBC4JDXMBAeJRc8AUEK/RYg0BGmvkh3aZUIlkAmCFk1OqyuEYoEl+ 25 | 2DsH26Glk978W4glHQi3PgKKa7NSvsHSd4BPGG6Vjs7jBh/LcSnT3iH5pHKkI/Tt 26 | sFWUKQ4Yz8DdUR02f890hOdlHG+Hz8DY33fS/Z8WoidMOJM+LHEqT9R4RAJi+puK 27 | 6Z8OTBCCJYMEgPx08swJbe9r7Q98n77ud+qyKPa/5jNJG3JYGSzsqhsyJXzCaQCl 28 | v1qVrzQ+qidO+tcousDMzNZ+AsdnLOGZ+UvKdx+l3kItj4AkIu2fkJb8sC/UP7ar 29 | EswtH8vJb8w03L8K/44Ql0cTDzHNyBJ5q/8xL/2M/L572Y8wmUcVsoPPw6+Js7l/ 30 | pSY3 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQD8s4EBIvHCsTWL 3 | eWGlLfKKBt1xJkijIQGogX/Iy5apUqv+XQIM/CUz+LAXT+xOgFsW3HRuQoKrvO1R 4 | GDDUi1hRqv6Y/4Mvwq7QXHNlVYvcQzUTa926omW9ukToXkjY5TRQLR6dJfnGDfi+ 5 | QdJBUYzZUlc0Ezpr7RpjF4+jYqN8U5eewU1W+UymDYY024fBpReg6F3bqmnflAOz 6 | LIA9W2JF9TeJ0H4+D4eneYgUksdnJFelWbDeE2WGX4iBr8f//Hmf31JyvUJDKXxn 7 | wQTlAqk9r3OeOw8k2J9IW51rWsEZEOLMx89+f6KZbnNdJIV//a0menM/2RplPmHB 8 | d7gctlsdqhiyw4l1cFgN/H2p3DjKsEq3XjPlkUOMK7tJ6UOcu1fVpn/5XZGuQ0eB 9 | 78bngysp2cCDYJvF5Z2uKvY7GUKeDN5TRoO40lzMfzfTngSb+67drvEmKm4MM5hl 10 | 7odNJcc0mAhS2ZmG+7Ts+U7e0Etj9XEyGDNS8JVgIfTlWhmCSbLbVNEW7yY2f/7s 11 | eIe3lAZMoSoZ2UUYniBw8Udurb901vjmE+vp3WI0LhQzkRFMylTau81BCFDA+JLZ 12 | /NeETwHAvF8H3hPqGcmkOFO7y0TFyTNEt8iC97GitJAW9iPYuPliM90gcmr979dV 13 | KSnqHof3dPsQfJwuVM/kdnaV7r53gQIDAQABAoICAQDziStypPLJ527rE/f+8OEm 14 | FKelPHgUfuLSOrukEFEKrhoD8i7fxME17R4H2YarwRgIWD39ZSv5xwIPfXjR3dko 15 | G9tyKA2OIdnIBNFRf7hidoLYTMRL8eaLitCOAQ/DuGFKQ7GVUdv9+8kV0umG+cj8 16 | SFayYTWUfdVIWpSbqZxVXVpqLXETuP8dqTsGBew3u5uh/081PG78gfFu5BxTBZcY 17 | RNNZhg2kUeMyi/WRnkN+K5AsUtwZqifV8IvmMDpXgkLUyKz012DcyUaT13mYG5Bv 18 | Wn/apqBZqksXuPNlWvlt5tAs+wQFrYxOwht8UI44Y4pT4v7fMaQ2noAnq/FL+pKj 19 | geLkTx6/9J1bN4bI1VnZmSSLVb2gC6XESaHO02vAzPlTtB//pomIgckEdCKQPEDI 20 | W58FiYumaIGf7jfXBz4oUW6YF2Tlm5SYUNCeF7Tnbdz/7vebodWENRMy1qhRB89x 21 | s8RDOwrGVt+JgQhDdnEOUh1SjlcYevBlGV0OaqXNV1XsSrJxVjWzWmmBJe7qvRdi 22 | pbYp6Kt+FZFDuimOyjiCPoiLBSoa5haQZLp8RMWa435fKGbddYryOtSb9CBxW/RP 23 | UnpulIiFELrO6sBKAlO9XSpsueSNersdZ6zhuET9JsWES33NDVJeNnLaZZILnGc9 24 | zPnBB0dqueD6C+IDv/8AAQKCAQEA/q5IS13dFb1QXU0h7FvtO3FDlHGIPZUflde1 25 | wsALUE4rqEMnHtntfvtHLiOhMHQ71MMv7l2GT4sRe6lsvVXnk1RUljhbj4rmEZVG 26 | fScCxauWkEMx9n6+Mw/TMCG8VEFDc7VWyuuPfQV7vt2vCLbz0gIFlLEIOSSquWet 27 | lWZfaBEexe8KEjZLqkMDKzSowVzGCgC3FWaxR4+lDHIn9IGLqGDfSYE65I3r6vYg 28 | nW45wOt5NbLiZf+RJycoBzm1PZl/PxZpSaROg0qDZX4D1LSc1gPDlLKe7sXRJEYK 29 | dZOko+k7fPjRisoYni5AZxhUwrkuTDX/bKR6f4/2kZKd2I53gQKCAQEA/gKYszzA 30 | g/2KyojTDN9uUmN4SThnFPJbJJoJHup4QL8EgNU0ytLt9GD05AuaPs0XFblCjNwY 31 | qJ64ouHHx8iXrBAA7yW6+BHswwH79S0cCXl6Xl1+2Uz1LYrogHvHSvzO2uNx1HOH 32 | v+bbKQ4LHIyKEOeNGD0JHIHckkZN0acjip7mnJJdPlCrP3REzzBLgeGnzljC2Ms2 33 | d+8mUKBzfO7fIksTBkDrSGVLr5oQv/Fqe+8HSdT0hQkCI7Bo7x0OmrTliQwLTphL 34 | nw61y+5wpg88sYZM0SxdPp2TOvfgcoc7ZS0HrdYrIHDDCrV/JQvGQ0AByidbUBI3 35 | 7fm2Q7gIrjAAAQKCAQAohxlwDN7Kv9aTElwsnbBRvkNv0uVIT3u4P5xoAmGKhPYD 36 | j7Qg/7MAewInwHm9GTIQOINfHjjqXYoJsLtiIdJ5KnlPcmZ3oDXeZG/UKKoTRKvw 37 | BxFjVWX1ADauOSAcFEqklh3aqsOptH6tr99TtrF0IOg8cjOJzGDyoiIIXUMfb2ID 38 | Q3fJ0CQYUxOlA3s7UgUdwGFiIXZimeQ858md6iOMRuYhb1Qs3LzHJiWoh8re/VnL 39 | hszqSFIT9fIzvCYwSEXshyd4FZJ86BWix/vaFGfE0tKDzizmeEpAyHiPn1Aa2Vmj 40 | GIFX4bMrMNcE0OVkG03XyNv9sOrhc8pb/gXqWTmBAoIBAE9zWnX90621WXs+Tt1g 41 | 6a4FhPNKHBwWLmIFeELeThzaYrs1dRzX2ywsQ40s/+MS3VyjJOjQUzoy40e3XXjl 42 | CmP8YX5sC85aNPdOIJQwtutTvu3TSsEHbE0BfPXrQYv4BW+74rf0JwrkV7rAtMMK 43 | RolBFAX32Wi8SdTK/r5MDDbouvNQaK/8JYRkhr1Tutp2TbmiU9fhwDjFafOgLF9w 44 | jAS6/Mlg9vcfEAxuIT0Ycxkuy9XRMWaHSc8F99yK9y121bEHPmYoBsdKn5yZCU03 45 | yOEyQ8bNnKDgQtQYAnFwUSi1bAh4y+aKvscTvCBHTY1tcOHda5dhC3N5PwRxhO2P 46 | AAECggEAP5JXVhNW5R7o4K5fx645lmN7TUZLJz81MAudO6LOF7fMFc8c1E1iuL3W 47 | QpMXRgXHqKzuvEirUNyqL/lysFruN9BQvdVes8tPzh0SlZbBgrNvWldsbALmi1mc 48 | 1Qet0HbcNK7gCh4tQWEyyrf6i1IYdCQ+QVZA99Rk/MP7IF5ASApUaC7jmwxD8Tci 49 | FQ6PhSSEKDc8VvW4ioezsNFWtNnpuLDK5wwYG4XUuEWFrDMaQH37RlAVQZgZcLKv 50 | RH9Y/h2RJ04xcvdD1tRHFRHyhm8fJU/XWhRdx/Px2Fe72Nm4Kj4Kv/ub6ppUFG6v 51 | iOi/KEjMxb+mQi4jBIKJq/nRXjglUg== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/selfsigned.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFCzCCAvOgAwIBAgIUI1eh28OnE/T/HBS9/6g9f4q3p/0wDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MDMyMDIwMjkzN1oYDzQ3NTcw 4 | MjEzMjAyOTM3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB 5 | AQUAA4ICDwAwggIKAoICAQC2r6mJFhnzwC0GhNt6beeTWP+8xnjOz0wO1LYZyDYy 6 | 3V1PMBxEwCWAi5xPCFyz2VTVH0yItOWPNCHTSF4XStmaAvjgKnn1wsQP0AgUXKHB 7 | ZEFSzdFKb/cdvHSP4ePf5l7YP/88X5A+W2zrksunZU47z3Z+gH4lYU6iIRrMwQyj 8 | Diy+DS36vC0u1LI9CmYqbHFpY9tI+Um8FhSqs85MZvSq+ar1h98BZakseXuW87R/ 9 | ghJ/iXptQ97W67tltLkv7cufcUGSdxmvln6zykpqlJhI9Rr3mSGyI5JpZ6V0974G 10 | +h1EjUYJZhkNUgzjo96i36EBvU+K7zEtZuA3KPGq2hBGSbDWaaqLrwPnUpbkY4KX 11 | 8+wcLbH4Be83p2Araf0EjlPsFQSPK8DPSxOuTzFrXoS6NmlVG0WWeeYMJpXmzthg 12 | egPmUJO5Ngb6d1PKG+DGc240/emmuLlZvC6GmBfY1eIMnymeAgexQv2mJDoc2V+n 13 | ZFdmPfA34sSuXsJhKCrOOKuT0HUcEEqd6jCEj7vU2hP9spUY3AYthTtnbQ5x6pGP 14 | veOA9cfTro3hRuampO7PgJVNmxkcKGI0aPXrOrxpSH8LNbbZCzrFPBxoszF5LbGS 15 | g42vpUtb52H+/dDFEuJSNMWUi9yFbPoocAyeP++LH+p2RJnq3eUvJXmUOc8mjwaa 16 | DQIDAQABo1MwUTAdBgNVHQ4EFgQUfkR9GE7Yc1tSGwFZEz4Xk7V2RhswHwYDVR0j 17 | BBgwFoAUfkR9GE7Yc1tSGwFZEz4Xk7V2RhswDwYDVR0TAQH/BAUwAwEB/zANBgkq 18 | hkiG9w0BAQsFAAOCAgEAKMO0HD503riWKRaikCFoA4n7LMUWaTdvZR/CZ/ZX0HJC 19 | 1PwBwk67+jLfjDa82cPH2yoxrK4PG21hO/d2+mwgDUh4jzHuJyPAMO45Hhq9zr8K 20 | SOQ+CmS7UwJZtLqVAwrxEUjRFpKeBEenNAtkrsTsv/JIp3iyu2FqVAq5mh1X/gDH 21 | CsByht2QaDBpZuTXhD0S6miFg21VT2aG1G55pUljOb/LjqEqxqWDZonsOMj8buTo 22 | NtAOCjxKIKKJfIz7hFDy+9Kecy/rmidQnRiNBm1dOjN9gk23FPu9EOBriL1jigYJ 23 | 3ws6WlgCQhhsTE/mCPyfxHA+8+GdaCc/L4qSNnxzlqN9wuxCpKQvrT2kTXIcjqVB 24 | QZz/Ii8Y2ZQR9paY/xCov/mri97uXrE4L2A1CodjcO7UCvswXNHrxgkOgmknb5WC 25 | jwbPCSpiYqnjtPvXWj2wygDX3456zZ2J7H7aEUAmOJfaL/xZpxJmejHtYedYINPZ 26 | /Zsk1bJTTuStXCvCgIiHIb9ILD7mq2LUDAh+RxANtqW2OAUzvqD/Xkg6zl6utQwJ 27 | UbIN1h2o0hl3qHZztVTaHRaBLgaPObhe2kFV9TioAbnJ1t04s4Xjw6ZFT3xhaySi 28 | mt5SBtT6qTkMohfYzZOe5+TPIMw87a0J3NwQNLl8kvRnyg00gBY4VU+l6MVTzYs= 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDDCNJTlNF 3 | Q1VSRSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTAgFw0xOTAzMjAyMDI5NTFa 4 | GA80NzU3MDIxMzIwMjk1MVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkq 5 | hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtq+piRYZ88AtBoTbem3nk1j/vMZ4zs9M 6 | DtS2Gcg2Mt1dTzAcRMAlgIucTwhcs9lU1R9MiLTljzQh00heF0rZmgL44Cp59cLE 7 | D9AIFFyhwWRBUs3RSm/3Hbx0j+Hj3+Ze2D//PF+QPlts65LLp2VOO892foB+JWFO 8 | oiEazMEMow4svg0t+rwtLtSyPQpmKmxxaWPbSPlJvBYUqrPOTGb0qvmq9YffAWWp 9 | LHl7lvO0f4ISf4l6bUPe1uu7ZbS5L+3Ln3FBkncZr5Z+s8pKapSYSPUa95khsiOS 10 | aWeldPe+BvodRI1GCWYZDVIM46Peot+hAb1Piu8xLWbgNyjxqtoQRkmw1mmqi68D 11 | 51KW5GOCl/PsHC2x+AXvN6dgK2n9BI5T7BUEjyvAz0sTrk8xa16EujZpVRtFlnnm 12 | DCaV5s7YYHoD5lCTuTYG+ndTyhvgxnNuNP3ppri5WbwuhpgX2NXiDJ8pngIHsUL9 13 | piQ6HNlfp2RXZj3wN+LErl7CYSgqzjirk9B1HBBKneowhI+71NoT/bKVGNwGLYU7 14 | Z20OceqRj73jgPXH066N4UbmpqTuz4CVTZsZHChiNGj16zq8aUh/CzW22Qs6xTwc 15 | aLMxeS2xkoONr6VLW+dh/v3QxRLiUjTFlIvchWz6KHAMnj/vix/qdkSZ6t3lLyV5 16 | lDnPJo8Gmg0CAwEAAaNQME4wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUfkR9GE7Y 17 | c1tSGwFZEz4Xk7V2RhswHwYDVR0jBBgwFoAUAuPXVKmD9xDl6lHNQH5w1IdFniww 18 | DQYJKoZIhvcNAQELBQADggIBAKlXVHoSPWaCh5nDbfe5fqfBxk4D0UV0a4t9tynG 19 | Lp3JkMZKwv5nY97l2b2WRPuYQVk9Dg9HISB+DB3ID9kMfxul3Nf/E4lBrF+VQkqO 20 | pw0VexiZt4XzWhzx5ZxKDKarmIa/WZms9AuGCa3kwtCopwTrq70AL6aIeJg93FJo 21 | qkisyvSX7EUg2i9vgV/QGth37WiJpcrcdUQFfXqjaID2gk+EfC6fNQP3eA5eTj9T 22 | Nn9Jssnkhu4EhVS+SiDKdI5F/xyAqZAyrkphZAztUtrcqUs3CPLFjI3c3bpU1LPF 23 | 9yCBajuLoubEtaTrkJGeney1CgEW3G2nca69LfCuJbwlIEKiAyw0t8/rBu4MeJy+ 24 | G6PO7py0xyOxFHQ4nmilEbeleSJfd2JssjUvDQAdS1K+MnQVWNk+aoF3fFjgBB4a 25 | MR7A6RYZ9fMjzJhgdA2Ucc5pO9BN/bIttkF4x7AHeV8WUrdCMC466fAdq4W4Tazh 26 | uMsQBqt7J9M28K2PICp3qUEMfY53tQZX39ablTSe/ZULTfjTG+36VPs6JLBCu9TV 27 | yPL0or3XqiFMUVxW2AN3uAW1LrklUavvJxGtq0h3DEA0e6KgFVWyP1GH9z2AMuuJ 28 | r2sqQdWajjDnX7A+irqMoXxpq5ZjMBZ+AZFB5VOcffVdr71p69EhG8snY33DjmUe 29 | kIdU 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/generated/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC2r6mJFhnzwC0G 3 | hNt6beeTWP+8xnjOz0wO1LYZyDYy3V1PMBxEwCWAi5xPCFyz2VTVH0yItOWPNCHT 4 | SF4XStmaAvjgKnn1wsQP0AgUXKHBZEFSzdFKb/cdvHSP4ePf5l7YP/88X5A+W2zr 5 | ksunZU47z3Z+gH4lYU6iIRrMwQyjDiy+DS36vC0u1LI9CmYqbHFpY9tI+Um8FhSq 6 | s85MZvSq+ar1h98BZakseXuW87R/ghJ/iXptQ97W67tltLkv7cufcUGSdxmvln6z 7 | ykpqlJhI9Rr3mSGyI5JpZ6V0974G+h1EjUYJZhkNUgzjo96i36EBvU+K7zEtZuA3 8 | KPGq2hBGSbDWaaqLrwPnUpbkY4KX8+wcLbH4Be83p2Araf0EjlPsFQSPK8DPSxOu 9 | TzFrXoS6NmlVG0WWeeYMJpXmzthgegPmUJO5Ngb6d1PKG+DGc240/emmuLlZvC6G 10 | mBfY1eIMnymeAgexQv2mJDoc2V+nZFdmPfA34sSuXsJhKCrOOKuT0HUcEEqd6jCE 11 | j7vU2hP9spUY3AYthTtnbQ5x6pGPveOA9cfTro3hRuampO7PgJVNmxkcKGI0aPXr 12 | OrxpSH8LNbbZCzrFPBxoszF5LbGSg42vpUtb52H+/dDFEuJSNMWUi9yFbPoocAye 13 | P++LH+p2RJnq3eUvJXmUOc8mjwaaDQIDAQABAoICAB4NZalMfnZvrbh90JEiVU8S 14 | +/b0+1iiF1P1QAObwPa7HadyNaRFq6Wq0craiE15ug/ZN0Dh6UWWgN398KSKRqOk 15 | S4as1iVMpe4n/bxDxMRIlsplW0GQi6lToCUvNspVXlLarEUlP1hSt+dE3cWqz6Td 16 | KxQCVT4W9NWM9piqRUbphQi6qE6v+ArVKIEMHeRqtHlu2Nr3T17mjv3J2G0PHr6l 17 | la8Qa+oR7DOJxH//lTjIj/x1BDPHT3gxjUFc6n3s82tGOVA70XjmG++9nmJMEYUm 18 | 9Qm4Iifo3CQEzd4hBxyktn3Zc6UGK6baUY59HZnTiev8VYJ4eLGMU+QfEypU9woh 19 | jF7fYgV0pmplA5uvyu1UHc2bqmQJNkyiSKf+kQn+Ooxg3qZ6PP4jMh/a3ma2Aq9b 20 | peDDJy1d4uqlvCwE7Rwe3wSpWktiQ5m4E03I+g4klo8WWtRf7dcsEbiV+6slTUnf 21 | 4Lm37reMal8SEuOLGsOTLL86dSxf6Xe+cWIB/j0j9OiChdybTHrbkcbmmWaw388T 22 | XPlNpnPDeFsVY36gtchQSCv4jWVBoTo31zdp49lywB7jeBLokePjVE9Xpp1RFxLm 23 | yCvzPEIIhM85Nv22g0LGbBluUVg5o+G8ZiuB5MAGQd7hhK3kTgOiHSKn6X6DgMMP 24 | 6O7/nBhH52IIHdRKbpXBAoIBAQDthYcg0pXdMhQjluuNt1HCAUSgPCv/Qd/rwxmy 25 | kCtmvPEDiFYhIUwIF1rs2wP87Gw8V2U/apu2wB5m1AwMNCAFOjL7zWjaXoxvYsnL 26 | xfsvMphtc6nluco8+AQs1t9tgXMtyTBce/9Tvym2oc1es7/6iedV6DHRW+g9Ix+W 27 | O7fLPlmSUWAsfcAoipecu6gEM5Qz3fEV4OI2Hh8ludadonoaTyUHYpi+4TPmztmG 28 | 9KTN8Og64584ZzYhOQFcdr509bZh/cEnqQ1oTeClHZh9VukMdpIA2qsZgXzkUKT6 29 | 8hl9KDO4RMGJZ88unb5yiWcXWfCLdlTU8VXUqRLkwMmHbRXRAoIBAQDE5gi+Ge3T 30 | P1aIi0UkwcXrvw/Xi8rZJinRqozOyjnXAi1Z7pi29OR+swWwaBSYP2SNBnVIrq6P 31 | wZdlIALdNbEU2lVcilzh5DiorIvbwW/a24V2RYeNEA+tWll9IMvLGclDJ1qQPFOE 32 | 9hMVJN55gCwiWE1eUl/xTyMSwSFWqMw97z8l1tG7XBJN042gPZL1TcLebqsYbfgh 33 | ZskCSeWki4TopMLf2sBKTK2KOxrPTVznv07mChKExj5jRyriYZGpwkXxKmnA8YA+ 34 | mAqtpGT2hcz44tTsfUNKWSZpYexf1WQfsZUUwshCLQc2QSVaCjY/DcpCrERoqxho 35 | ToYkBmYkSYN9AoIBAQDlAEJiIOaglvHXxmBhCAfpdnOF0b3Rot+TXU1fXaSarzvn 36 | 3gC8fG/jtTDS/+5+YiuQmepiFBUBQ4WMOpFLCs58e7pAD7EyKMpZcfOBZeZhgPR5 37 | QnThqhkUY0MTQC+2UoKL+FeKM3recYZ0MNfioIdNtLu3leDCK0xj9YM0w2rWp8yQ 38 | R6jj/HHSJJ08XakGM/yFbUxNfb5b0vrQ0rXa0ZXL29aloigGkPYLaA9OEHz8mXIt 39 | pNwT81669U0cqtfByE0JeTBLXrAwijU2vKwS3EJg48iszib8tl2Fe0M1N0nf42zK 40 | EnBOXCnile/SWits1igXLgVoMS5BlKlEp6Ml0MShAoIBACTyra1OnExJ1GknCUCO 41 | hB9q6QQfnV8GRE6t3GJpnFT2eaAon8ulRMNpwKWLwmGXc7zq+1M6RFOBlnFJgoAU 42 | NTieh+onpKpW8zLjvhLsx5qgGvlIrtkPrIFxNN3AK73fBt9tIRdz2pBWxNnw4zaV 43 | kLKZM1uBxbM4kHJSf3kNj9YHcSgemZi4/E5SZn8tTshtYSXwSnb5G9jYutuNFmS8 44 | MaKeWFs9Z1wJ637G/I7uC4MJaTrNpQ/5S3fBwRBeEsFyTUGmHdYw+0nDqi1Pbgq8 45 | rOv/VMlT8C6hcA3SbFLHblRsNHeC3aVdYb8SlnnA9ND8O3orbWLeyGO6aH2WHdCt 46 | YtkCggEAJPfr8O129Nq7p0ke48Sh/jrrqJ0FwVdmEBYZWDkkjpp9bz493x+rFpDQ 47 | S8Qq3MOgO+BAHTBACh/Y2kHDUAr43ZNfusMd1WXJS9Y3ORVI3CPVDrVQpzsAX5t+ 48 | dCcdmHiWzFXob4yTKRcwNku44JoxWRA/pzSft/H5ha4UTwWxBPaIiDvvhsNX6GRU 49 | xqN8ZD5d9FH3Q4KJa5NI0j90gW6YbD8wnzSWcbOF1AqosjaZ/om9aT3eIYe6bhEX 50 | eqb2hwFVYfE6jaaIrhBtetyFEWinjfSFhOIxaZ8LFLVCqGeM2DNhF/PRQTMf42aE 51 | ISDbj3Gvk5Gx42y1LbTSnVg8SHm4Vw== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/openssl-exts.cnf: -------------------------------------------------------------------------------- 1 | [ca] 2 | basicConstraints=critical,CA:true 3 | subjectKeyIdentifier=hash 4 | authorityKeyIdentifier=keyid:always,issuer:always 5 | 6 | [cert] 7 | basicConstraints=critical,CA:false 8 | subjectKeyIdentifier=hash 9 | authorityKeyIdentifier=keyid,issuer 10 | -------------------------------------------------------------------------------- /spec/fixtures/tiny.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnunemaker/httparty/9417ce158d732fecc753c656accb59c470cfb6f8/spec/fixtures/tiny.gif -------------------------------------------------------------------------------- /spec/fixtures/twitter.csv: -------------------------------------------------------------------------------- 1 | "name","url","id","description","protected","screen_name","followers_count","profile_image_url","location" 2 | "Magic 8 Bot",,"17656026","ask me a question","false","magic8bot","90","http://s3.amazonaws.com/twitter_production/profile_images/65565851/8ball_large_normal.jpg", -------------------------------------------------------------------------------- /spec/fixtures/undefined_method_add_node_for_nil.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/httparty/cookie_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::CookieHash do 4 | before(:each) do 5 | @cookie_hash = HTTParty::CookieHash.new 6 | end 7 | 8 | describe "#add_cookies" do 9 | describe "with a hash" do 10 | it "should add new key/value pairs to the hash" do 11 | @cookie_hash.add_cookies(foo: "bar") 12 | @cookie_hash.add_cookies(rofl: "copter") 13 | expect(@cookie_hash.length).to eql(2) 14 | end 15 | 16 | it "should overwrite any existing key" do 17 | @cookie_hash.add_cookies(foo: "bar") 18 | @cookie_hash.add_cookies(foo: "copter") 19 | expect(@cookie_hash.length).to eql(1) 20 | expect(@cookie_hash[:foo]).to eql("copter") 21 | end 22 | end 23 | 24 | describe "with a string" do 25 | it "should add new key/value pairs to the hash" do 26 | @cookie_hash.add_cookies("first=one; second=two; third") 27 | expect(@cookie_hash[:first]).to eq('one') 28 | expect(@cookie_hash[:second]).to eq('two') 29 | expect(@cookie_hash[:third]).to eq(nil) 30 | end 31 | 32 | it "should overwrite any existing key" do 33 | @cookie_hash[:foo] = 'bar' 34 | @cookie_hash.add_cookies("foo=tar") 35 | expect(@cookie_hash.length).to eql(1) 36 | expect(@cookie_hash[:foo]).to eql("tar") 37 | end 38 | 39 | it "should handle '=' within cookie value" do 40 | @cookie_hash.add_cookies("first=one=1; second=two=2==") 41 | expect(@cookie_hash.keys).to include(:first, :second) 42 | expect(@cookie_hash[:first]).to eq('one=1') 43 | expect(@cookie_hash[:second]).to eq('two=2==') 44 | end 45 | 46 | it "should handle an empty cookie parameter" do 47 | @cookie_hash.add_cookies("first=one; domain=mydomain.com; path=/; ; SameSite; Secure") 48 | expect(@cookie_hash.keys).to include(:first, :domain, :path, :SameSite, :Secure) 49 | end 50 | end 51 | 52 | describe 'with other class' do 53 | it "should error" do 54 | expect { 55 | @cookie_hash.add_cookies([]) 56 | }.to raise_error(RuntimeError) 57 | end 58 | end 59 | end 60 | 61 | # The regexen are required because Hashes aren't ordered, so a test against 62 | # a hardcoded string was randomly failing. 63 | describe "#to_cookie_string" do 64 | before(:each) do 65 | @cookie_hash.add_cookies(foo: "bar") 66 | @cookie_hash.add_cookies(rofl: "copter") 67 | @s = @cookie_hash.to_cookie_string 68 | end 69 | 70 | it "should format the key/value pairs, delimited by semi-colons" do 71 | expect(@s).to match(/foo=bar/) 72 | expect(@s).to match(/rofl=copter/) 73 | expect(@s).to match(/^\w+=\w+; \w+=\w+$/) 74 | end 75 | 76 | it "should not include client side only cookies" do 77 | @cookie_hash.add_cookies(path: "/") 78 | @s = @cookie_hash.to_cookie_string 79 | expect(@s).not_to match(/path=\//) 80 | end 81 | 82 | it "should not include SameSite attribute" do 83 | @cookie_hash.add_cookies(samesite: "Strict") 84 | @s = @cookie_hash.to_cookie_string 85 | expect(@s).not_to match(/samesite=Strict/) 86 | end 87 | 88 | it "should not include client side only cookies even when attributes use camal case" do 89 | @cookie_hash.add_cookies(Path: "/") 90 | @s = @cookie_hash.to_cookie_string 91 | expect(@s).not_to match(/Path=\//) 92 | end 93 | 94 | it "should not mutate the hash" do 95 | original_hash = { 96 | "session" => "91e25e8b-6e32-418d-c72f-2d18adf041cd", 97 | "Max-Age" => "15552000", 98 | "cart" => "91e25e8b-6e32-418d-c72f-2d18adf041cd", 99 | "httponly" => nil, 100 | "Path" => "/", 101 | "secure" => nil, 102 | } 103 | 104 | cookie_hash = HTTParty::CookieHash[original_hash] 105 | 106 | cookie_hash.to_cookie_string 107 | 108 | expect(cookie_hash).to eq(original_hash) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/httparty/decompressor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::Decompressor do 4 | describe '.SupportedEncodings' do 5 | it 'returns a hash' do 6 | expect(HTTParty::Decompressor::SupportedEncodings).to be_instance_of(Hash) 7 | end 8 | end 9 | 10 | describe '#decompress' do 11 | let(:body) { 'body' } 12 | let(:encoding) { 'none' } 13 | let(:decompressor) { described_class.new(body, encoding) } 14 | subject { decompressor.decompress } 15 | 16 | shared_examples 'returns nil' do 17 | it { expect(subject).to be_nil } 18 | end 19 | 20 | shared_examples 'returns the body' do 21 | it { expect(subject).to eq 'body' } 22 | end 23 | 24 | context 'when body is nil' do 25 | let(:body) { nil } 26 | it_behaves_like 'returns nil' 27 | end 28 | 29 | context 'when body is blank' do 30 | let(:body) { ' ' } 31 | it { expect(subject).to eq ' ' } 32 | end 33 | 34 | context 'when encoding is nil' do 35 | let(:encoding) { nil } 36 | it_behaves_like 'returns the body' 37 | end 38 | 39 | context 'when encoding is blank' do 40 | let(:encoding) { ' ' } 41 | it_behaves_like 'returns the body' 42 | end 43 | 44 | context 'when encoding is "none"' do 45 | let(:encoding) { 'none' } 46 | it_behaves_like 'returns the body' 47 | end 48 | 49 | context 'when encoding is "identity"' do 50 | let(:encoding) { 'identity' } 51 | it_behaves_like 'returns the body' 52 | end 53 | 54 | context 'when encoding is unsupported' do 55 | let(:encoding) { 'invalid' } 56 | it_behaves_like 'returns nil' 57 | end 58 | 59 | context 'when encoding is "br"' do 60 | let(:encoding) { 'br' } 61 | 62 | context 'when brotli gem not included' do 63 | it_behaves_like 'returns nil' 64 | end 65 | 66 | context 'when brotli included' do 67 | before do 68 | dbl = double('Brotli') 69 | expect(dbl).to receive(:inflate).with('body').and_return('foobar') 70 | stub_const('Brotli', dbl) 71 | end 72 | 73 | it { expect(subject).to eq 'foobar' } 74 | end 75 | 76 | context 'when brotli raises error' do 77 | before do 78 | dbl = double('brotli') 79 | expect(dbl).to receive(:inflate).with('body') { raise RuntimeError.new('brotli error') } 80 | stub_const('Brotli', dbl) 81 | end 82 | 83 | it { expect(subject).to eq nil } 84 | end 85 | end 86 | 87 | context 'when encoding is "compress"' do 88 | let(:encoding) { 'compress' } 89 | 90 | context 'when LZW gem not included' do 91 | it_behaves_like 'returns nil' 92 | end 93 | 94 | context 'when ruby-lzws included' do 95 | before do 96 | dbl = double('lzws') 97 | expect(dbl).to receive(:decompress).with('body').and_return('foobar') 98 | stub_const('LZWS::String', dbl) 99 | end 100 | 101 | it { expect(subject).to eq 'foobar' } 102 | end 103 | 104 | context 'when ruby-lzws raises error' do 105 | before do 106 | dbl = double('lzws') 107 | expect(dbl).to receive(:decompress).with('body') { raise RuntimeError.new('brotli error') } 108 | stub_const('LZWS::String', dbl) 109 | end 110 | 111 | it { expect(subject).to eq nil } 112 | end 113 | 114 | context 'when compress-lzw included' do 115 | before do 116 | dbl2 = double('lzw2') 117 | dbl = double('lzw1', new: dbl2) 118 | expect(dbl2).to receive(:decompress).with('body').and_return('foobar') 119 | stub_const('LZW::Simple', dbl) 120 | end 121 | 122 | it { expect(subject).to eq 'foobar' } 123 | end 124 | 125 | context 'when compress-lzw raises error' do 126 | before do 127 | dbl2 = double('lzw2') 128 | dbl = double('lzw1', new: dbl2) 129 | expect(dbl2).to receive(:decompress).with('body') { raise RuntimeError.new('brotli error') } 130 | stub_const('LZW::Simple', dbl) 131 | end 132 | end 133 | end 134 | 135 | context 'when encoding is "zstd"' do 136 | let(:encoding) { 'zstd' } 137 | 138 | context 'when zstd-ruby gem not included' do 139 | it_behaves_like 'returns nil' 140 | end 141 | 142 | context 'when zstd-ruby included' do 143 | before do 144 | dbl = double('Zstd') 145 | expect(dbl).to receive(:decompress).with('body').and_return('foobar') 146 | stub_const('Zstd', dbl) 147 | end 148 | 149 | it { expect(subject).to eq 'foobar' } 150 | end 151 | 152 | context 'when zstd raises error' do 153 | before do 154 | dbl = double('Zstd') 155 | expect(dbl).to receive(:decompress).with('body') { raise RuntimeError.new('zstd error') } 156 | stub_const('Zstd', dbl) 157 | end 158 | 159 | it { expect(subject).to eq nil } 160 | end 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /spec/httparty/exception_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::Error do 4 | subject { described_class } 5 | 6 | describe '#ancestors' do 7 | subject { super().ancestors } 8 | it { is_expected.to include(StandardError) } 9 | end 10 | 11 | describe HTTParty::UnsupportedFormat do 12 | describe '#ancestors' do 13 | subject { super().ancestors } 14 | it { is_expected.to include(HTTParty::Error) } 15 | end 16 | end 17 | 18 | describe HTTParty::UnsupportedURIScheme do 19 | describe '#ancestors' do 20 | subject { super().ancestors } 21 | it { is_expected.to include(HTTParty::Error) } 22 | end 23 | end 24 | 25 | describe HTTParty::ResponseError do 26 | describe '#ancestors' do 27 | subject { super().ancestors } 28 | it { is_expected.to include(HTTParty::Error) } 29 | end 30 | end 31 | 32 | describe HTTParty::RedirectionTooDeep do 33 | describe '#ancestors' do 34 | subject { super().ancestors } 35 | it { is_expected.to include(HTTParty::ResponseError) } 36 | end 37 | end 38 | 39 | describe HTTParty::DuplicateLocationHeader do 40 | describe '#ancestors' do 41 | subject { super().ancestors } 42 | it { is_expected.to include(HTTParty::ResponseError) } 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/httparty/hash_conversions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::HashConversions do 4 | describe ".to_params" do 5 | it "creates a params string from a hash" do 6 | hash = { 7 | name: "bob", 8 | address: { 9 | street: '111 ruby ave.', 10 | city: 'ruby central', 11 | phones: ['111-111-1111', '222-222-2222'] 12 | } 13 | } 14 | expect(HTTParty::HashConversions.to_params(hash)).to eq("name=bob&address%5Bstreet%5D=111%20ruby%20ave.&address%5Bcity%5D=ruby%20central&address%5Bphones%5D%5B%5D=111-111-1111&address%5Bphones%5D%5B%5D=222-222-2222") 15 | end 16 | 17 | context "nested params" do 18 | it 'creates a params string from a hash' do 19 | hash = { marketing_event: { marketed_resources: [ {type:"product", id: 57474842640 } ] } } 20 | expect(HTTParty::HashConversions.to_params(hash)).to eq("marketing_event%5Bmarketed_resources%5D%5B%5D%5Btype%5D=product&marketing_event%5Bmarketed_resources%5D%5B%5D%5Bid%5D=57474842640") 21 | end 22 | end 23 | end 24 | 25 | describe ".normalize_param" do 26 | context "value is an array" do 27 | it "creates a params string" do 28 | expect( 29 | HTTParty::HashConversions.normalize_param(:people, ["Bob Jones", "Mike Smith"]) 30 | ).to eq("people%5B%5D=Bob%20Jones&people%5B%5D=Mike%20Smith&") 31 | end 32 | end 33 | 34 | context "value is an empty array" do 35 | it "creates a params string" do 36 | expect( 37 | HTTParty::HashConversions.normalize_param(:people, []) 38 | ).to eq("people%5B%5D=&") 39 | end 40 | end 41 | 42 | context "value is hash" do 43 | it "creates a params string" do 44 | expect( 45 | HTTParty::HashConversions.normalize_param(:person, { name: "Bob Jones" }) 46 | ).to eq("person%5Bname%5D=Bob%20Jones&") 47 | end 48 | end 49 | 50 | context "value is a string" do 51 | it "creates a params string" do 52 | expect( 53 | HTTParty::HashConversions.normalize_param(:name, "Bob Jones") 54 | ).to eq("name=Bob%20Jones&") 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/httparty/headers_processor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::HeadersProcessor do 4 | subject(:headers) { options[:headers] } 5 | let(:options) { { headers: {} } } 6 | let(:global_headers) { {} } 7 | 8 | before { described_class.new(global_headers, options).call } 9 | 10 | context 'when headers are not set at all' do 11 | it 'returns empty hash' do 12 | expect(headers).to eq({}) 13 | end 14 | end 15 | 16 | context 'when only global headers are set' do 17 | let(:global_headers) { { accept: 'text/html' } } 18 | 19 | it 'returns stringified global headers' do 20 | expect(headers).to eq('accept' => 'text/html') 21 | end 22 | end 23 | 24 | context 'when only request specific headers are set' do 25 | let(:options) { { headers: {accept: 'text/html' } } } 26 | 27 | it 'returns stringified request specific headers' do 28 | expect(headers).to eq('accept' => 'text/html') 29 | end 30 | end 31 | 32 | context 'when global and request specific headers are set' do 33 | let(:global_headers) { { 'x-version' => '123' } } 34 | 35 | let(:options) { { headers: { accept: 'text/html' } } } 36 | 37 | it 'returns merged global and request specific headers' do 38 | expect(headers).to eq('accept' => 'text/html', 'x-version' => '123') 39 | end 40 | end 41 | 42 | context 'when headers are dynamic' do 43 | let(:global_headers) { {'x-version' => -> { 'abc'.reverse } } } 44 | 45 | let(:options) do 46 | { body: '123', 47 | headers: { sum: lambda { |options| options[:body].chars.map(&:to_i).inject(:+) } } } 48 | end 49 | 50 | it 'returns processed global and request specific headers' do 51 | expect(headers).to eq('sum' => 6, 'x-version' => 'cba') 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/httparty/logger/apache_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::Logger::ApacheFormatter do 4 | let(:subject) { described_class.new(logger_double, :info) } 5 | let(:logger_double) { double('Logger') } 6 | let(:request_double) { double('Request', http_method: Net::HTTP::Get, path: "http://my.domain.com/my_path") } 7 | let(:request_time) { Time.new.strftime("%Y-%m-%d %H:%M:%S %z") } 8 | 9 | before do 10 | expect(logger_double).to receive(:info).with(log_message) 11 | end 12 | 13 | describe "#format" do 14 | let(:log_message) { "[HTTParty] [#{request_time}] 302 \"GET http://my.domain.com/my_path\" - " } 15 | 16 | it "formats a response in a style that resembles apache's access log" do 17 | response_double = double( 18 | code: 302, 19 | :[] => nil 20 | ) 21 | 22 | subject.format(request_double, response_double) 23 | end 24 | 25 | context 'when there is a parsed response' do 26 | let(:log_message) { "[HTTParty] [#{request_time}] 200 \"GET http://my.domain.com/my_path\" 512 "} 27 | 28 | it "can handle the Content-Length header" do 29 | # Simulate a parsed response that is an array, where accessing a string key will raise an error. See Issue #299. 30 | response_double = double( 31 | code: 200, 32 | headers: { 'Content-Length' => 512 } 33 | ) 34 | allow(response_double).to receive(:[]).with('Content-Length').and_raise(TypeError.new('no implicit conversion of String into Integer')) 35 | 36 | subject.format(request_double, response_double) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/httparty/logger/curl_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::Logger::CurlFormatter do 4 | describe "#format" do 5 | let(:logger) { double('Logger') } 6 | let(:response_object) { Net::HTTPOK.new('1.1', 200, 'OK') } 7 | let(:parsed_response) { lambda { {"foo" => "bar"} } } 8 | 9 | let(:response) do 10 | HTTParty::Response.new(request, response_object, parsed_response) 11 | end 12 | 13 | let(:request) do 14 | HTTParty::Request.new(Net::HTTP::Get, 'http://foo.bar.com/') 15 | end 16 | 17 | subject { described_class.new(logger, :info) } 18 | 19 | before do 20 | allow(logger).to receive(:info) 21 | allow(request).to receive(:raw_body).and_return('content') 22 | allow(response_object).to receive_messages(body: "{foo:'bar'}") 23 | response_object['header-key'] = 'header-value' 24 | 25 | subject.format request, response 26 | end 27 | 28 | context 'when request is logged' do 29 | context "and request's option 'base_uri' is not present" do 30 | it 'logs url' do 31 | expect(logger).to have_received(:info).with(/\[HTTParty\] \[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\ [+-]\d{4}\] > GET http:\/\/foo.bar.com/) 32 | end 33 | end 34 | 35 | context "and request's option 'base_uri' is present" do 36 | let(:request) do 37 | HTTParty::Request.new(Net::HTTP::Get, '/path', base_uri: 'http://foo.bar.com') 38 | end 39 | 40 | it 'logs url' do 41 | expect(logger).to have_received(:info).with(/\[HTTParty\] \[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\ [+-]\d{4}\] > GET http:\/\/foo.bar.com\/path/) 42 | end 43 | end 44 | 45 | context 'and headers are not present' do 46 | it 'not log Headers' do 47 | expect(logger).not_to have_received(:info).with(/Headers/) 48 | end 49 | end 50 | 51 | context 'and headers are present' do 52 | let(:request) do 53 | HTTParty::Request.new(Net::HTTP::Get, '/path', base_uri: 'http://foo.bar.com', headers: { key: 'value' }) 54 | end 55 | 56 | it 'logs Headers' do 57 | expect(logger).to have_received(:info).with(/Headers/) 58 | end 59 | 60 | it 'logs headers keys' do 61 | expect(logger).to have_received(:info).with(/key: value/) 62 | end 63 | end 64 | 65 | context 'and query is not present' do 66 | it 'not logs Query' do 67 | expect(logger).not_to have_received(:info).with(/Query/) 68 | end 69 | end 70 | 71 | context 'and query is present' do 72 | let(:request) do 73 | HTTParty::Request.new(Net::HTTP::Get, '/path', query: { key: 'value' }) 74 | end 75 | 76 | it 'logs Query' do 77 | expect(logger).to have_received(:info).with(/Query/) 78 | end 79 | 80 | it 'logs query params' do 81 | expect(logger).to have_received(:info).with(/key: value/) 82 | end 83 | end 84 | 85 | context 'when request raw_body is present' do 86 | it 'not logs request body' do 87 | expect(logger).to have_received(:info).with(/content/) 88 | end 89 | end 90 | end 91 | 92 | context 'when response is logged' do 93 | it 'logs http version and response code' do 94 | expect(logger).to have_received(:info).with(/HTTP\/1.1 200/) 95 | end 96 | 97 | it 'logs headers' do 98 | expect(logger).to have_received(:info).with(/Header-key: header-value/) 99 | end 100 | 101 | it 'logs body' do 102 | expect(logger).to have_received(:info).with(/{foo:'bar'}/) 103 | end 104 | end 105 | 106 | it "formats a response in a style that resembles a -v curl" do 107 | logger_double = double 108 | expect(logger_double).to receive(:info).with( 109 | /\[HTTParty\] \[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\ [+-]\d{4}\] > GET http:\/\/localhost/) 110 | 111 | subject = described_class.new(logger_double, :info) 112 | 113 | stub_http_response_with("example.html") 114 | 115 | response = HTTParty::Request.new.perform 116 | subject.format(response.request, response) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/httparty/logger/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::Logger do 4 | describe ".build" do 5 | subject { HTTParty::Logger } 6 | 7 | it "defaults level to :info" do 8 | logger_double = double 9 | expect(subject.build(logger_double, nil, nil).level).to eq(:info) 10 | end 11 | 12 | it "defaults format to :apache" do 13 | logger_double = double 14 | expect(subject.build(logger_double, nil, nil)).to be_an_instance_of(HTTParty::Logger::ApacheFormatter) 15 | end 16 | 17 | it "builds :curl style logger" do 18 | logger_double = double 19 | expect(subject.build(logger_double, nil, :curl)).to be_an_instance_of(HTTParty::Logger::CurlFormatter) 20 | end 21 | 22 | it "builds :logstash style logger" do 23 | logger_double = double 24 | expect(subject.build(logger_double, nil, :logstash)).to be_an_instance_of(HTTParty::Logger::LogstashFormatter) 25 | end 26 | 27 | it "builds :custom style logger" do 28 | CustomFormatter = Class.new(HTTParty::Logger::CurlFormatter) 29 | HTTParty::Logger.add_formatter(:custom, CustomFormatter) 30 | 31 | logger_double = double 32 | expect(subject.build(logger_double, nil, :custom)). 33 | to be_an_instance_of(CustomFormatter) 34 | end 35 | it "raises error when formatter exists" do 36 | CustomFormatter2= Class.new(HTTParty::Logger::CurlFormatter) 37 | HTTParty::Logger.add_formatter(:custom2, CustomFormatter2) 38 | 39 | expect{ HTTParty::Logger.add_formatter(:custom2, CustomFormatter2) }. 40 | to raise_error HTTParty::Error 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/httparty/logger/logstash_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'spec_helper' 3 | 4 | RSpec.describe HTTParty::Logger::LogstashFormatter do 5 | let(:severity) { :info } 6 | let(:http_method) { 'GET' } 7 | let(:path) { 'http://my.domain.com/my_path' } 8 | let(:logger_double) { double('Logger') } 9 | let(:request_double) { double('Request', http_method: Net::HTTP::Get, path: "#{path}") } 10 | let(:request_time) { Time.new.strftime("%Y-%m-%d %H:%M:%S %z") } 11 | let(:log_message) do 12 | { 13 | '@timestamp' => request_time, 14 | '@version' => 1, 15 | 'content_length' => content_length || '-', 16 | 'http_method' => http_method, 17 | 'message' => message, 18 | 'path' => path, 19 | 'response_code' => response_code, 20 | 'severity' => severity, 21 | 'tags' => ['HTTParty'], 22 | }.to_json 23 | end 24 | 25 | subject { described_class.new(logger_double, severity) } 26 | 27 | before do 28 | expect(logger_double).to receive(:info).with(log_message) 29 | end 30 | 31 | describe "#format" do 32 | let(:response_code) { 302 } 33 | let(:content_length) { '-' } 34 | let(:message) { "[HTTParty] #{response_code} \"#{http_method} #{path}\" #{content_length} " } 35 | 36 | it "formats a response to be compatible with Logstash" do 37 | response_double = double( 38 | code: response_code, 39 | :[] => nil 40 | ) 41 | 42 | subject.format(request_double, response_double) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/httparty/net_digest_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Net::HTTPHeader::DigestAuthenticator do 4 | def setup_digest(response) 5 | digest = Net::HTTPHeader::DigestAuthenticator.new("Mufasa", 6 | "Circle Of Life", "GET", "/dir/index.html", response) 7 | allow(digest).to receive(:random).and_return("deadbeef") 8 | allow(Digest::MD5).to receive(:hexdigest) { |str| "md5(#{str})" } 9 | digest 10 | end 11 | 12 | def authorization_header 13 | @digest.authorization_header.join(", ") 14 | end 15 | 16 | def cookie_header 17 | @digest.cookie_header 18 | end 19 | 20 | context 'Net::HTTPHeader#digest_auth' do 21 | let(:headers) { 22 | (Class.new do 23 | include Net::HTTPHeader 24 | def initialize 25 | @header = {} 26 | @path = '/' 27 | @method = 'GET' 28 | end 29 | end).new 30 | } 31 | 32 | let(:response){ 33 | (Class.new do 34 | include Net::HTTPHeader 35 | def initialize 36 | @header = {} 37 | self['WWW-Authenticate'] = 38 | 'Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"' 39 | end 40 | end).new 41 | } 42 | 43 | it 'should set the authorization header' do 44 | expect(headers['authorization']).to be_nil 45 | headers.digest_auth('user','pass', response) 46 | expect(headers['authorization']).to_not be_empty 47 | end 48 | end 49 | 50 | context "with a cookie value in the response header" do 51 | before do 52 | @digest = setup_digest({ 53 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com"', 54 | 'Set-Cookie' => 'custom-cookie=1234567' 55 | }) 56 | end 57 | 58 | it "should set cookie header" do 59 | expect(cookie_header).to include('custom-cookie=1234567') 60 | end 61 | end 62 | 63 | context "without a cookie value in the response header" do 64 | before do 65 | @digest = setup_digest({ 66 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com"' 67 | }) 68 | end 69 | 70 | it "should set empty cookie header array" do 71 | expect(cookie_header).to eql [] 72 | end 73 | end 74 | 75 | context "with an opaque value in the response header" do 76 | before do 77 | @digest = setup_digest({ 78 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", opaque="solid"' 79 | }) 80 | end 81 | 82 | it "should set opaque" do 83 | expect(authorization_header).to include('opaque="solid"') 84 | end 85 | end 86 | 87 | context "without an opaque valid in the response header" do 88 | before do 89 | @digest = setup_digest({ 90 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com"' 91 | }) 92 | end 93 | 94 | it "should not set opaque" do 95 | expect(authorization_header).not_to include("opaque=") 96 | end 97 | end 98 | 99 | context "with specified quality of protection (qop)" do 100 | before do 101 | @digest = setup_digest({ 102 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth"' 103 | }) 104 | end 105 | 106 | it "should set prefix" do 107 | expect(authorization_header).to match(/^Digest /) 108 | end 109 | 110 | it "should set username" do 111 | expect(authorization_header).to include('username="Mufasa"') 112 | end 113 | 114 | it "should set digest-uri" do 115 | expect(authorization_header).to include('uri="/dir/index.html"') 116 | end 117 | 118 | it "should set qop" do 119 | expect(authorization_header).to include('qop="auth"') 120 | end 121 | 122 | it "should set cnonce" do 123 | expect(authorization_header).to include('cnonce="md5(deadbeef)"') 124 | end 125 | 126 | it "should set nonce-count" do 127 | expect(authorization_header).to include("nc=00000001") 128 | end 129 | 130 | it "should set response" do 131 | request_digest = "md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" 132 | expect(authorization_header).to include(%(response="#{request_digest}")) 133 | end 134 | end 135 | 136 | context "when quality of protection (qop) is unquoted" do 137 | before do 138 | @digest = setup_digest({ 139 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop=auth' 140 | }) 141 | end 142 | 143 | it "should still set qop" do 144 | expect(authorization_header).to include('qop="auth"') 145 | end 146 | end 147 | 148 | context "with unspecified quality of protection (qop)" do 149 | before do 150 | @digest = setup_digest({ 151 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE"' 152 | }) 153 | end 154 | 155 | it "should set prefix" do 156 | expect(authorization_header).to match(/^Digest /) 157 | end 158 | 159 | it "should set username" do 160 | expect(authorization_header).to include('username="Mufasa"') 161 | end 162 | 163 | it "should set digest-uri" do 164 | expect(authorization_header).to include('uri="/dir/index.html"') 165 | end 166 | 167 | it "should not set qop" do 168 | expect(authorization_header).not_to include("qop=") 169 | end 170 | 171 | it "should not set cnonce" do 172 | expect(authorization_header).not_to include("cnonce=") 173 | end 174 | 175 | it "should not set nonce-count" do 176 | expect(authorization_header).not_to include("nc=") 177 | end 178 | 179 | it "should set response" do 180 | request_digest = "md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:md5(GET:/dir/index.html))" 181 | expect(authorization_header).to include(%(response="#{request_digest}")) 182 | end 183 | end 184 | 185 | context "with http basic auth response when net digest auth expected" do 186 | it "should not fail" do 187 | @digest = setup_digest({ 188 | 'www-authenticate' => 'WWW-Authenticate: Basic realm="testrealm.com""' 189 | }) 190 | 191 | expect(authorization_header).to include("Digest") 192 | end 193 | end 194 | 195 | context "with multiple authenticate headers" do 196 | before do 197 | @digest = setup_digest({ 198 | 'www-authenticate' => 'NTLM, Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth"' 199 | }) 200 | end 201 | 202 | it "should set prefix" do 203 | expect(authorization_header).to match(/^Digest /) 204 | end 205 | 206 | it "should set username" do 207 | expect(authorization_header).to include('username="Mufasa"') 208 | end 209 | 210 | it "should set digest-uri" do 211 | expect(authorization_header).to include('uri="/dir/index.html"') 212 | end 213 | 214 | it "should set qop" do 215 | expect(authorization_header).to include('qop="auth"') 216 | end 217 | 218 | it "should set cnonce" do 219 | expect(authorization_header).to include('cnonce="md5(deadbeef)"') 220 | end 221 | 222 | it "should set nonce-count" do 223 | expect(authorization_header).to include("nc=00000001") 224 | end 225 | 226 | it "should set response" do 227 | request_digest = "md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" 228 | expect(authorization_header).to include(%(response="#{request_digest}")) 229 | end 230 | end 231 | 232 | context "with algorithm specified" do 233 | before do 234 | @digest = setup_digest({ 235 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth", algorithm=MD5' 236 | }) 237 | end 238 | 239 | it "should recognise algorithm was specified" do 240 | expect( @digest.send :algorithm_present? ).to be(true) 241 | end 242 | 243 | it "should set the algorithm header" do 244 | expect(authorization_header).to include('algorithm="MD5"') 245 | end 246 | end 247 | 248 | context "with md5-sess algorithm specified" do 249 | before do 250 | @digest = setup_digest({ 251 | 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth", algorithm=MD5-sess' 252 | }) 253 | end 254 | 255 | it "should recognise algorithm was specified" do 256 | expect( @digest.send :algorithm_present? ).to be(true) 257 | end 258 | 259 | it "should set the algorithm header" do 260 | expect(authorization_header).to include('algorithm="MD5-sess"') 261 | end 262 | 263 | it "should set response using md5-sess algorithm" do 264 | request_digest = "md5(md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:md5(deadbeef)):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" 265 | expect(authorization_header).to include(%(response="#{request_digest}")) 266 | end 267 | 268 | end 269 | 270 | end 271 | -------------------------------------------------------------------------------- /spec/httparty/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'multi_xml' 3 | 4 | RSpec.describe HTTParty::Parser do 5 | describe ".SupportedFormats" do 6 | it "returns a hash" do 7 | expect(HTTParty::Parser::SupportedFormats).to be_instance_of(Hash) 8 | end 9 | end 10 | 11 | describe ".call" do 12 | it "generates an HTTParty::Parser instance with the given body and format" do 13 | expect(HTTParty::Parser).to receive(:new).with('body', :plain).and_return(double(parse: nil)) 14 | HTTParty::Parser.call('body', :plain) 15 | end 16 | 17 | it "calls #parse on the parser" do 18 | parser = double('Parser') 19 | expect(parser).to receive(:parse) 20 | allow(HTTParty::Parser).to receive_messages(new: parser) 21 | parser = HTTParty::Parser.call('body', :plain) 22 | end 23 | end 24 | 25 | describe ".formats" do 26 | it "returns the SupportedFormats constant" do 27 | expect(HTTParty::Parser.formats).to eq(HTTParty::Parser::SupportedFormats) 28 | end 29 | 30 | it "returns the SupportedFormats constant for subclasses" do 31 | klass = Class.new(HTTParty::Parser) 32 | klass::SupportedFormats = { "application/atom+xml" => :atom } 33 | 34 | expect(klass.formats).to eq({"application/atom+xml" => :atom}) 35 | end 36 | end 37 | 38 | describe ".format_from_mimetype" do 39 | it "returns a symbol representing the format mimetype" do 40 | expect(HTTParty::Parser.format_from_mimetype("text/plain")).to eq(:plain) 41 | end 42 | 43 | it "returns nil when the mimetype is not supported" do 44 | expect(HTTParty::Parser.format_from_mimetype("application/atom+xml")).to be_nil 45 | end 46 | end 47 | 48 | describe ".supported_formats" do 49 | it "returns a unique set of supported formats represented by symbols" do 50 | expect(HTTParty::Parser.supported_formats).to eq(HTTParty::Parser::SupportedFormats.values.uniq) 51 | end 52 | end 53 | 54 | describe ".supports_format?" do 55 | it "returns true for a supported format" do 56 | allow(HTTParty::Parser).to receive_messages(supported_formats: [:json]) 57 | expect(HTTParty::Parser.supports_format?(:json)).to be_truthy 58 | end 59 | 60 | it "returns false for an unsupported format" do 61 | allow(HTTParty::Parser).to receive_messages(supported_formats: []) 62 | expect(HTTParty::Parser.supports_format?(:json)).to be_falsey 63 | end 64 | end 65 | 66 | describe "#parse" do 67 | it "attempts to parse supported formats" do 68 | parser = HTTParty::Parser.new('body', :json) 69 | allow(parser).to receive_messages(supports_format?: true) 70 | 71 | expect(parser).to receive(:parse_supported_format) 72 | parser.parse 73 | end 74 | 75 | it "returns the unparsed body when the format is unsupported" do 76 | parser = HTTParty::Parser.new('body', :json) 77 | allow(parser).to receive_messages(supports_format?: false) 78 | 79 | expect(parser.parse).to eq(parser.body) 80 | end 81 | 82 | it "returns nil for an empty body" do 83 | parser = HTTParty::Parser.new('', :json) 84 | expect(parser.parse).to be_nil 85 | end 86 | 87 | it "returns nil for a nil body" do 88 | parser = HTTParty::Parser.new(nil, :json) 89 | expect(parser.parse).to be_nil 90 | end 91 | 92 | it "returns nil for a 'null' body" do 93 | parser = HTTParty::Parser.new("null", :json) 94 | expect(parser.parse).to be_nil 95 | end 96 | 97 | it "returns nil for a body with spaces only" do 98 | parser = HTTParty::Parser.new(" ", :json) 99 | expect(parser.parse).to be_nil 100 | end 101 | 102 | it "does not raise exceptions for bodies with invalid encodings" do 103 | parser = HTTParty::Parser.new("\x80", :invalid_format) 104 | expect(parser.parse).to_not be_nil 105 | end 106 | 107 | it "ignores utf-8 bom" do 108 | parser = HTTParty::Parser.new("\xEF\xBB\xBF\{\"hi\":\"yo\"\}", :json) 109 | expect(parser.parse).to eq({"hi"=>"yo"}) 110 | end 111 | 112 | it "parses ascii 8bit encoding" do 113 | parser = HTTParty::Parser.new( 114 | "{\"currency\":\"\xE2\x82\xAC\"}".force_encoding('ASCII-8BIT'), 115 | :json 116 | ) 117 | expect(parser.parse).to eq({"currency" => "€"}) 118 | end 119 | 120 | it "parses frozen strings" do 121 | parser = HTTParty::Parser.new('{"a":1}'.freeze, :json) 122 | expect(parser.parse).to eq("a" => 1) 123 | end 124 | end 125 | 126 | describe "#supports_format?" do 127 | it "utilizes the class method to determine if the format is supported" do 128 | expect(HTTParty::Parser).to receive(:supports_format?).with(:json) 129 | parser = HTTParty::Parser.new('body', :json) 130 | parser.send(:supports_format?) 131 | end 132 | end 133 | 134 | describe "#parse_supported_format" do 135 | it "calls the parser for the given format" do 136 | parser = HTTParty::Parser.new('body', :json) 137 | expect(parser).to receive(:json) 138 | parser.send(:parse_supported_format) 139 | end 140 | 141 | context "when a parsing method does not exist for the given format" do 142 | it "raises an exception" do 143 | parser = HTTParty::Parser.new('body', :atom) 144 | expect do 145 | parser.send(:parse_supported_format) 146 | end.to raise_error(NotImplementedError, "HTTParty::Parser has not implemented a parsing method for the :atom format.") 147 | end 148 | 149 | it "raises a useful exception message for subclasses" do 150 | atom_parser = Class.new(HTTParty::Parser) do 151 | def self.name 152 | 'AtomParser' 153 | end 154 | end 155 | parser = atom_parser.new 'body', :atom 156 | expect do 157 | parser.send(:parse_supported_format) 158 | end.to raise_error(NotImplementedError, "AtomParser has not implemented a parsing method for the :atom format.") 159 | end 160 | end 161 | end 162 | 163 | context "parsers" do 164 | subject do 165 | HTTParty::Parser.new('body', nil) 166 | end 167 | 168 | it "parses xml with MultiXml" do 169 | expect(MultiXml).to receive(:parse).with('body') 170 | subject.send(:xml) 171 | end 172 | 173 | it "parses json with JSON" do 174 | expect(JSON).to receive(:parse).with('body', :quirks_mode => true, :allow_nan => true) 175 | subject.send(:json) 176 | end 177 | 178 | it "parses html by simply returning the body" do 179 | expect(subject.send(:html)).to eq('body') 180 | end 181 | 182 | it "parses plain text by simply returning the body" do 183 | expect(subject.send(:plain)).to eq('body') 184 | end 185 | 186 | it "parses csv with CSV" do 187 | require 'csv' 188 | expect(CSV).to receive(:parse).with('body') 189 | subject.send(:csv) 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /spec/httparty/request/body_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | RSpec.describe HTTParty::Request::Body do 5 | describe '#call' do 6 | let(:options) { {} } 7 | 8 | subject { described_class.new(params, **options).call } 9 | 10 | context 'when params is string' do 11 | let(:params) { 'name=Bob%20Jones' } 12 | 13 | it { is_expected.to eq params } 14 | end 15 | 16 | context 'when params is hash' do 17 | let(:params) { { people: ["Bob Jones", "Mike Smith"] } } 18 | let(:converted_params) { "people%5B%5D=Bob%20Jones&people%5B%5D=Mike%20Smith"} 19 | 20 | it { is_expected.to eq converted_params } 21 | 22 | context 'when params has file' do 23 | before do 24 | allow(HTTParty::Request::MultipartBoundary) 25 | .to receive(:generate).and_return("------------------------c772861a5109d5ef") 26 | end 27 | 28 | let(:file) { File.open('spec/fixtures/tiny.gif') } 29 | let(:params) do 30 | { 31 | user: { 32 | avatar: file, 33 | first_name: 'John', 34 | last_name: 'Doe', 35 | enabled: true 36 | } 37 | } 38 | end 39 | let(:expected_file_name) { 'tiny.gif' } 40 | let(:expected_file_contents) { "GIF89a\u0001\u0000\u0001\u0000\u0000\xFF\u0000,\u0000\u0000\u0000\u0000\u0001\u0000\u0001\u0000\u0000\u0002\u0000;" } 41 | let(:expected_content_type) { 'image/gif' } 42 | let(:multipart_params) do 43 | "--------------------------c772861a5109d5ef\r\n" \ 44 | "Content-Disposition: form-data; name=\"user[avatar]\"; filename=\"#{expected_file_name}\"\r\n" \ 45 | "Content-Type: #{expected_content_type}\r\n" \ 46 | "\r\n" \ 47 | "#{expected_file_contents}\r\n" \ 48 | "--------------------------c772861a5109d5ef\r\n" \ 49 | "Content-Disposition: form-data; name=\"user[first_name]\"\r\n" \ 50 | "\r\n" \ 51 | "John\r\n" \ 52 | "--------------------------c772861a5109d5ef\r\n" \ 53 | "Content-Disposition: form-data; name=\"user[last_name]\"\r\n" \ 54 | "\r\n" \ 55 | "Doe\r\n" \ 56 | "--------------------------c772861a5109d5ef\r\n" \ 57 | "Content-Disposition: form-data; name=\"user[enabled]\"\r\n" \ 58 | "\r\n" \ 59 | "true\r\n" \ 60 | "--------------------------c772861a5109d5ef--\r\n" 61 | end 62 | 63 | it { is_expected.to eq multipart_params } 64 | 65 | it { expect { subject }.not_to change { file.pos } } 66 | 67 | context 'when passing multipart as an option' do 68 | let(:options) { { force_multipart: true } } 69 | let(:params) do 70 | { 71 | user: { 72 | first_name: 'John', 73 | last_name: 'Doe', 74 | enabled: true 75 | } 76 | } 77 | end 78 | let(:multipart_params) do 79 | "--------------------------c772861a5109d5ef\r\n" \ 80 | "Content-Disposition: form-data; name=\"user[first_name]\"\r\n" \ 81 | "\r\n" \ 82 | "John\r\n" \ 83 | "--------------------------c772861a5109d5ef\r\n" \ 84 | "Content-Disposition: form-data; name=\"user[last_name]\"\r\n" \ 85 | "\r\n" \ 86 | "Doe\r\n" \ 87 | "--------------------------c772861a5109d5ef\r\n" \ 88 | "Content-Disposition: form-data; name=\"user[enabled]\"\r\n" \ 89 | "\r\n" \ 90 | "true\r\n" \ 91 | "--------------------------c772861a5109d5ef--\r\n" 92 | end 93 | 94 | it { is_expected.to eq multipart_params } 95 | 96 | end 97 | 98 | context 'file object responds to original_filename' do 99 | let(:some_temp_file) { Tempfile.new(['some_temp_file','.gif']) } 100 | let(:expected_file_name) { "some_temp_file.gif" } 101 | let(:expected_file_contents) { "Hello" } 102 | let(:file) { double(:mocked_action_dispatch, path: some_temp_file.path, original_filename: 'some_temp_file.gif', read: expected_file_contents) } 103 | 104 | before { some_temp_file.write('Hello') } 105 | 106 | it { is_expected.to eq multipart_params } 107 | end 108 | 109 | context 'when file name contains [ " \r \n ]' do 110 | let(:options) { { force_multipart: true } } 111 | let(:some_temp_file) { Tempfile.new(['basefile', '.txt']) } 112 | let(:file_content) { 'test' } 113 | let(:raw_filename) { "dummy=tampering.sh\"; \r\ndummy=a.txt" } 114 | let(:expected_file_name) { 'dummy=tampering.sh%22; %0D%0Adummy=a.txt' } 115 | let(:file) { double(:mocked_action_dispatch, path: some_temp_file.path, original_filename: raw_filename, read: file_content) } 116 | let(:params) do 117 | { 118 | user: { 119 | attachment_file: file, 120 | enabled: true 121 | } 122 | } 123 | end 124 | let(:multipart_params) do 125 | "--------------------------c772861a5109d5ef\r\n" \ 126 | "Content-Disposition: form-data; name=\"user[attachment_file]\"; filename=\"#{expected_file_name}\"\r\n" \ 127 | "Content-Type: text/plain\r\n" \ 128 | "\r\n" \ 129 | "test\r\n" \ 130 | "--------------------------c772861a5109d5ef\r\n" \ 131 | "Content-Disposition: form-data; name=\"user[enabled]\"\r\n" \ 132 | "\r\n" \ 133 | "true\r\n" \ 134 | "--------------------------c772861a5109d5ef--\r\n" 135 | end 136 | 137 | it { is_expected.to eq multipart_params } 138 | 139 | end 140 | end 141 | end 142 | end 143 | 144 | describe '#multipart?' do 145 | let(:force_multipart) { false } 146 | let(:file) { File.open('spec/fixtures/tiny.gif') } 147 | 148 | subject { described_class.new(params, force_multipart: force_multipart).multipart? } 149 | 150 | context 'when params does not respond to to_hash' do 151 | let(:params) { 'name=Bob%20Jones' } 152 | 153 | it { is_expected.to be false } 154 | end 155 | 156 | context 'when params responds to to_hash' do 157 | class HashLike 158 | def initialize(hash) 159 | @hash = hash 160 | end 161 | 162 | def to_hash 163 | @hash 164 | end 165 | end 166 | 167 | class ArrayLike 168 | def initialize(ary) 169 | @ary = ary 170 | end 171 | 172 | def to_ary 173 | @ary 174 | end 175 | end 176 | 177 | context 'when force_multipart is true' do 178 | let(:params) { { name: 'Bob Jones' } } 179 | let(:force_multipart) { true } 180 | 181 | it { is_expected.to be true } 182 | end 183 | 184 | context 'when it does not contain a file' do 185 | let(:hash_like_param) { HashLike.new(first: 'Bob', last: ArrayLike.new(['Jones'])) } 186 | let(:params) { { name: ArrayLike.new([hash_like_param]) } } 187 | 188 | it { is_expected.to eq false } 189 | end 190 | 191 | context 'when it contains file' do 192 | let(:hash_like_param) { HashLike.new(first: 'Bob', last: 'Jones', file: ArrayLike.new([file])) } 193 | let(:params) { { name: ArrayLike.new([hash_like_param]) } } 194 | 195 | it { is_expected.to be true } 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/httparty/response_fragment_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper')) 2 | 3 | RSpec.describe HTTParty::ResponseFragment do 4 | it "access to fragment" do 5 | fragment = HTTParty::ResponseFragment.new("chunk", nil, nil) 6 | expect(fragment).to eq("chunk") 7 | end 8 | 9 | it "has access to delegators" do 10 | response = double(code: '200') 11 | connection = double 12 | fragment = HTTParty::ResponseFragment.new("chunk", response, connection) 13 | expect(fragment.code).to eq(200) 14 | expect(fragment.http_response).to eq response 15 | expect(fragment.connection).to eq connection 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/httparty/ssl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HTTParty::Request do 4 | context "SSL certificate verification" do 5 | before do 6 | WebMock.disable! 7 | end 8 | 9 | after do 10 | WebMock.enable! 11 | end 12 | 13 | it "should fail when no trusted CA list is specified, by default" do 14 | expect do 15 | ssl_verify_test(nil, nil, "selfsigned.crt") 16 | end.to raise_error OpenSSL::SSL::SSLError 17 | end 18 | 19 | it "should work when no trusted CA list is specified, when the verify option is set to false" do 20 | expect(ssl_verify_test(nil, nil, "selfsigned.crt", verify: false).parsed_response).to eq({'success' => true}) 21 | end 22 | 23 | it "should fail when no trusted CA list is specified, with a bogus hostname, by default" do 24 | expect do 25 | ssl_verify_test(nil, nil, "bogushost.crt") 26 | end.to raise_error OpenSSL::SSL::SSLError 27 | end 28 | 29 | it "should work when no trusted CA list is specified, even with a bogus hostname, when the verify option is set to true" do 30 | expect(ssl_verify_test(nil, nil, "bogushost.crt", verify: false).parsed_response).to eq({'success' => true}) 31 | end 32 | 33 | it "should work when using ssl_ca_file with a self-signed CA" do 34 | expect(ssl_verify_test(:ssl_ca_file, "selfsigned.crt", "selfsigned.crt").parsed_response).to eq({'success' => true}) 35 | end 36 | 37 | it "should work when using ssl_ca_file with a certificate authority" do 38 | expect(ssl_verify_test(:ssl_ca_file, "ca.crt", "server.crt").parsed_response).to eq({'success' => true}) 39 | end 40 | 41 | it "should work when using ssl_ca_path with a certificate authority" do 42 | http = Net::HTTP.new('www.google.com', 443) 43 | response = double(Net::HTTPResponse, :[] => '', body: '', to_hash: {}, delete: nil) 44 | allow(http).to receive(:request).and_return(response) 45 | expect(Net::HTTP).to receive(:new).with('www.google.com', 443).and_return(http) 46 | expect(http).to receive(:ca_path=).with('/foo/bar') 47 | HTTParty.get('https://www.google.com', ssl_ca_path: '/foo/bar') 48 | end 49 | 50 | it "should fail when using ssl_ca_file and the server uses an unrecognized certificate authority" do 51 | expect do 52 | ssl_verify_test(:ssl_ca_file, "ca.crt", "selfsigned.crt") 53 | end.to raise_error(OpenSSL::SSL::SSLError) 54 | end 55 | 56 | it "should fail when using ssl_ca_path and the server uses an unrecognized certificate authority" do 57 | expect do 58 | ssl_verify_test(:ssl_ca_path, ".", "selfsigned.crt") 59 | end.to raise_error(OpenSSL::SSL::SSLError) 60 | end 61 | 62 | it "should fail when using ssl_ca_file and the server uses a bogus hostname" do 63 | expect do 64 | ssl_verify_test(:ssl_ca_file, "ca.crt", "bogushost.crt") 65 | end.to raise_error(OpenSSL::SSL::SSLError) 66 | end 67 | 68 | it "should fail when using ssl_ca_path and the server uses a bogus hostname" do 69 | expect do 70 | ssl_verify_test(:ssl_ca_path, ".", "bogushost.crt") 71 | end.to raise_error(OpenSSL::SSL::SSLError) 72 | end 73 | 74 | it "should provide the certificate used by the server via peer_cert" do 75 | peer_cert = nil 76 | ssl_verify_test(:ssl_ca_file, "ca.crt", "server.crt") do |response| 77 | peer_cert ||= response.connection.peer_cert 78 | end 79 | expect(peer_cert).to be_a OpenSSL::X509::Certificate 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start 3 | 4 | require "httparty" 5 | require 'webmock/rspec' 6 | 7 | def file_fixture(filename) 8 | open(File.join(File.dirname(__FILE__), 'fixtures', "#{filename}")).read 9 | end 10 | 11 | Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each {|f| require f} 12 | 13 | RSpec.configure do |config| 14 | config.include HTTParty::StubResponse 15 | config.include HTTParty::SSLTestHelper 16 | 17 | config.expect_with :rspec do |expectations| 18 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 19 | end 20 | 21 | config.mock_with :rspec do |mocks| 22 | mocks.verify_partial_doubles = false 23 | end 24 | 25 | config.filter_run :focus 26 | config.run_all_when_everything_filtered = true 27 | 28 | config.disable_monkey_patching! 29 | 30 | config.warnings = true 31 | 32 | if config.files_to_run.one? 33 | config.default_formatter = 'doc' 34 | end 35 | 36 | config.profile_examples = 10 37 | 38 | config.order = :random 39 | 40 | config.before(:each) do 41 | # Reset default_cert_store cache 42 | HTTParty::ConnectionAdapter.instance_variable_set(:@default_cert_store, nil) 43 | end 44 | 45 | Kernel.srand config.seed 46 | end 47 | 48 | RSpec::Matchers.define :use_ssl do 49 | match(&:use_ssl?) 50 | end 51 | 52 | RSpec::Matchers.define :use_cert_store do |cert_store| 53 | match do |connection| 54 | connection.cert_store == cert_store 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/support/ssl_test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module HTTParty 4 | module SSLTestHelper 5 | def ssl_verify_test(mode, ca_basename, server_cert_filename, options = {}, &block) 6 | options = { 7 | format: :json, 8 | timeout: 30 9 | }.merge(options) 10 | 11 | if mode 12 | ca_path = File.expand_path("../../fixtures/ssl/generated/#{ca_basename}", __FILE__) 13 | raise ArgumentError.new("#{ca_path} does not exist") unless File.exist?(ca_path) 14 | options[mode] = ca_path 15 | end 16 | 17 | begin 18 | test_server = SSLTestServer.new( 19 | rsa_key: File.read(File.expand_path("../../fixtures/ssl/generated/server.key", __FILE__)), 20 | cert: File.read(File.expand_path("../../fixtures/ssl/generated/#{server_cert_filename}", __FILE__))) 21 | 22 | test_server.start 23 | 24 | if mode 25 | ca_path = File.expand_path("../../fixtures/ssl/generated/#{ca_basename}", __FILE__) 26 | raise ArgumentError.new("#{ca_path} does not exist") unless File.exist?(ca_path) 27 | return HTTParty.get("https://localhost:#{test_server.port}/", options, &block) 28 | else 29 | return HTTParty.get("https://localhost:#{test_server.port}/", options, &block) 30 | end 31 | ensure 32 | test_server.stop if test_server 33 | end 34 | 35 | test_server = SSLTestServer.new({ 36 | rsa_key: path.join('server.key').read, 37 | cert: path.join(server_cert_filename).read 38 | }) 39 | 40 | test_server.start 41 | 42 | HTTParty.get("https://localhost:#{test_server.port}/", options, &block) 43 | ensure 44 | test_server.stop if test_server 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/support/ssl_test_server.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'socket' 3 | require 'thread' 4 | 5 | # NOTE: This code is garbage. It probably has deadlocks, it might leak 6 | # threads, and otherwise cause problems in a real system. It's really only 7 | # intended for testing HTTParty. 8 | class SSLTestServer 9 | attr_accessor :ctx # SSLContext object 10 | attr_reader :port 11 | 12 | def initialize(options = {}) 13 | @ctx = OpenSSL::SSL::SSLContext.new 14 | @ctx.cert = OpenSSL::X509::Certificate.new(options[:cert]) 15 | @ctx.key = OpenSSL::PKey::RSA.new(options[:rsa_key]) 16 | @ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE # Don't verify client certificate 17 | @port = options[:port] || 0 18 | @thread = nil 19 | @stopping_mutex = Mutex.new 20 | @stopping = false 21 | end 22 | 23 | def start 24 | @raw_server = TCPServer.new(@port) 25 | 26 | if @port == 0 27 | @port = Socket.getnameinfo(@raw_server.getsockname, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV)[1].to_i 28 | end 29 | 30 | @ssl_server = OpenSSL::SSL::SSLServer.new(@raw_server, @ctx) 31 | 32 | @stopping_mutex.synchronize { 33 | return if @stopping 34 | @thread = Thread.new { thread_main } 35 | } 36 | 37 | nil 38 | end 39 | 40 | def stop 41 | @stopping_mutex.synchronize { 42 | return if @stopping 43 | @stopping = true 44 | } 45 | @thread.join 46 | end 47 | 48 | private 49 | 50 | def thread_main 51 | until @stopping_mutex.synchronize { @stopping } 52 | (rr, _, _) = select([@ssl_server.to_io], nil, nil, 0.1) 53 | 54 | next unless rr && rr.include?(@ssl_server.to_io) 55 | 56 | socket = @ssl_server.accept 57 | 58 | Thread.new { 59 | header = [] 60 | 61 | until (line = socket.readline).rstrip.empty? 62 | header << line 63 | end 64 | 65 | response = < 2 | 3 | 4 | 5 | HTTParty by John Nunemaker 6 | 7 | 8 | 9 | 10 |
11 | 21 | 22 |
23 |

Install

24 |
$ sudo gem install httparty
25 | 26 |

Some Quick Examples

27 | 28 |

The following is a simple example of wrapping Twitter's API for posting updates.

29 | 30 |
class Twitter
31 |   include HTTParty
32 |   base_uri 'twitter.com'
33 |   basic_auth 'username', 'password'
34 | end
35 | 
36 | Twitter.post('/statuses/update.json', query: {status: "It's an HTTParty and everyone is invited!"})
37 | 38 |

That is really it! The object returned is a ruby hash that is decoded from Twitter's json response. JSON parsing is used because of the .json extension in the path of the request. You can also explicitly set a format (see the examples).

39 | 40 |

That works and all but what if you don't want to embed your username and password in the class? Below is an example to fix that:

41 | 42 |
class Twitter
43 |   include HTTParty
44 |   base_uri 'twitter.com'
45 | 
46 |   def initialize(u, p)
47 |     @auth = {username: u, password: p}
48 |   end
49 | 
50 |   def post(text)
51 |     options = { query: {status: text}, basic_auth: @auth }
52 |     self.class.post('/statuses/update.json', options)
53 |   end
54 | end
55 | 
56 | Twitter.new('username', 'password').post("It's an HTTParty and everyone is invited!")
57 | 58 |

More Examples: There are several examples in the gem itself.

59 | 60 |

Support

61 |

Conversations welcome in the google group and bugs/features over at Github.

62 | 63 | 64 |
65 | 66 | 70 |
71 | 72 | 73 | --------------------------------------------------------------------------------