mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-08-10 00:01:27 -04:00
multipart supported by default
the plugin was now moved to the transcoder layer, where it is available from the get-go.
This commit is contained in:
parent
896914e189
commit
3f73d2e3ce
@ -7,6 +7,7 @@
|
||||
* `:total_timeout` option has been removed (no session-wide timeout supported, use `:request_timeout`).
|
||||
* `:read_timeout` and `:write_timeout` are now set to 60 seconds by default, and preferred over `:operation_timeout`;
|
||||
* the exception being in the `:stream` plugin, as the response is theoretically endless (so `:read_timeout` is unset).
|
||||
* The `:multipart` plugin is now loaded by default.
|
||||
|
||||
### plugins
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Plugins
|
||||
#
|
||||
# This plugin adds support for passing `http-form_data` objects (like file objects) as "multipart/form-data";
|
||||
#
|
||||
# HTTPX.post(URL, form: form: { image: HTTP::FormData::File.new("path/to/file")})
|
||||
#
|
||||
# https://gitlab.com/os85/httpx/wikis/Multipart-Uploads
|
||||
#
|
||||
module Multipart
|
||||
MULTIPART_VALUE_COND = lambda do |value|
|
||||
value.respond_to?(:read) ||
|
||||
(value.respond_to?(:to_hash) &&
|
||||
value.key?(:body) &&
|
||||
(value.key?(:filename) || value.key?(:content_type)))
|
||||
end
|
||||
|
||||
class << self
|
||||
def normalize_keys(key, value, &block)
|
||||
Transcoder.normalize_keys(key, value, MULTIPART_VALUE_COND, &block)
|
||||
end
|
||||
|
||||
def load_dependencies(*)
|
||||
# :nocov:
|
||||
begin
|
||||
unless defined?(HTTP::FormData)
|
||||
# in order not to break legacy code, we'll keep loading http/form_data for them.
|
||||
require "http/form_data"
|
||||
warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
|
||||
"https://os85.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
|
||||
"If you'd like to stop seeing this message, require 'http/form_data' yourself."
|
||||
end
|
||||
rescue LoadError
|
||||
end
|
||||
# :nocov:
|
||||
require "httpx/plugins/multipart/encoder"
|
||||
require "httpx/plugins/multipart/decoder"
|
||||
require "httpx/plugins/multipart/part"
|
||||
require "httpx/plugins/multipart/mime_type_detector"
|
||||
end
|
||||
end
|
||||
|
||||
module RequestBodyMethods
|
||||
private
|
||||
|
||||
def initialize_body(options)
|
||||
return FormTranscoder.encode(options.form) if options.form
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
module ResponseMethods
|
||||
def form
|
||||
decode(FormTranscoder)
|
||||
end
|
||||
end
|
||||
|
||||
module FormTranscoder
|
||||
module_function
|
||||
|
||||
def encode(form)
|
||||
if multipart?(form)
|
||||
Encoder.new(form)
|
||||
else
|
||||
Transcoder::Form::Encoder.new(form)
|
||||
end
|
||||
end
|
||||
|
||||
def decode(response)
|
||||
content_type = response.content_type.mime_type
|
||||
|
||||
case content_type
|
||||
when "application/x-www-form-urlencoded"
|
||||
Transcoder::Form.decode(response)
|
||||
when "multipart/form-data"
|
||||
Decoder.new(response)
|
||||
else
|
||||
raise Error, "invalid form mime type (#{content_type})"
|
||||
end
|
||||
end
|
||||
|
||||
def multipart?(data)
|
||||
data.any? do |_, v|
|
||||
MULTIPART_VALUE_COND.call(v) ||
|
||||
(v.respond_to?(:to_ary) && v.to_ary.any?(&MULTIPART_VALUE_COND)) ||
|
||||
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| MULTIPART_VALUE_COND.call(e) })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
register_plugin :multipart, Multipart
|
||||
end
|
||||
end
|
@ -1,137 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "tempfile"
|
||||
require "delegate"
|
||||
|
||||
module HTTPX::Plugins
|
||||
module Multipart
|
||||
class FilePart < SimpleDelegator
|
||||
attr_reader :original_filename, :content_type
|
||||
|
||||
def initialize(filename, content_type)
|
||||
@original_filename = filename
|
||||
@content_type = content_type
|
||||
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
||||
super(@file)
|
||||
end
|
||||
end
|
||||
|
||||
class Decoder
|
||||
include HTTPX::Utils
|
||||
|
||||
CRLF = "\r\n"
|
||||
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
||||
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
||||
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
||||
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
||||
WINDOW_SIZE = 2 << 14
|
||||
|
||||
def initialize(response)
|
||||
@boundary = begin
|
||||
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
||||
raise Error, "no boundary declared in content-type header" unless m
|
||||
|
||||
m.strip
|
||||
end
|
||||
@buffer = "".b
|
||||
@parts = {}
|
||||
@intermediate_boundary = "--#{@boundary}"
|
||||
@state = :idle
|
||||
end
|
||||
|
||||
def call(response, *)
|
||||
response.body.each do |chunk|
|
||||
@buffer << chunk
|
||||
|
||||
parse
|
||||
end
|
||||
|
||||
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
||||
|
||||
@parts
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse
|
||||
case @state
|
||||
when :idle
|
||||
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
||||
|
||||
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
||||
|
||||
@state = :part_header
|
||||
when :part_header
|
||||
idx = @buffer.index("#{CRLF}#{CRLF}")
|
||||
|
||||
# raise Error, "couldn't parse part headers" unless idx
|
||||
return unless idx
|
||||
|
||||
head = @buffer.byteslice(0..idx + 4 - 1)
|
||||
|
||||
@buffer = @buffer.byteslice(head.bytesize..-1)
|
||||
|
||||
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
||||
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
||||
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
||||
name.gsub!(/\\(.)/, "\\1")
|
||||
name
|
||||
else
|
||||
name = head[MULTIPART_CONTENT_ID, 1]
|
||||
end
|
||||
|
||||
filename = HTTPX::Utils.get_filename(head)
|
||||
|
||||
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
||||
|
||||
@current = name
|
||||
|
||||
@parts[name] = if filename
|
||||
FilePart.new(filename, content_type)
|
||||
else
|
||||
"".b
|
||||
end
|
||||
|
||||
@state = :part_body
|
||||
when :part_body
|
||||
part = @parts[@current]
|
||||
|
||||
body_separator = if part.is_a?(FilePart)
|
||||
"#{CRLF}#{CRLF}"
|
||||
else
|
||||
CRLF
|
||||
end
|
||||
idx = @buffer.index(body_separator)
|
||||
|
||||
if idx
|
||||
payload = @buffer.byteslice(0..idx - 1)
|
||||
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
||||
part << payload
|
||||
part.rewind if part.respond_to?(:rewind)
|
||||
@state = :parse_boundary
|
||||
else
|
||||
part << @buffer
|
||||
@buffer.clear
|
||||
end
|
||||
when :parse_boundary
|
||||
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
||||
|
||||
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
||||
|
||||
if @buffer == "--"
|
||||
@buffer.clear
|
||||
@state = :done
|
||||
return
|
||||
elsif @buffer.start_with?(CRLF)
|
||||
@buffer = @buffer.byteslice(2..-1)
|
||||
@state = :part_header
|
||||
else
|
||||
return
|
||||
end
|
||||
when :done
|
||||
raise Error, "parsing should have been over by now"
|
||||
end until @buffer.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -2,57 +2,77 @@
|
||||
|
||||
require "forwardable"
|
||||
require "uri"
|
||||
require_relative "multipart"
|
||||
|
||||
module HTTPX::Transcoder
|
||||
module Form
|
||||
module_function
|
||||
module HTTPX
|
||||
module Transcoder
|
||||
module Form
|
||||
module_function
|
||||
|
||||
PARAM_DEPTH_LIMIT = 32
|
||||
PARAM_DEPTH_LIMIT = 32
|
||||
|
||||
class Encoder
|
||||
extend Forwardable
|
||||
class Encoder
|
||||
extend Forwardable
|
||||
|
||||
def_delegator :@raw, :to_s
|
||||
def_delegator :@raw, :to_s
|
||||
|
||||
def_delegator :@raw, :to_str
|
||||
def_delegator :@raw, :to_str
|
||||
|
||||
def_delegator :@raw, :bytesize
|
||||
def_delegator :@raw, :bytesize
|
||||
|
||||
def initialize(form)
|
||||
@raw = form.each_with_object("".b) do |(key, val), buf|
|
||||
HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
|
||||
buf << "&" unless buf.empty?
|
||||
buf << URI.encode_www_form_component(k)
|
||||
buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil?
|
||||
def initialize(form)
|
||||
@raw = form.each_with_object("".b) do |(key, val), buf|
|
||||
HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
|
||||
buf << "&" unless buf.empty?
|
||||
buf << URI.encode_www_form_component(k)
|
||||
buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def content_type
|
||||
"application/x-www-form-urlencoded"
|
||||
end
|
||||
end
|
||||
|
||||
module Decoder
|
||||
module_function
|
||||
|
||||
def call(response, *)
|
||||
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
|
||||
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def content_type
|
||||
"application/x-www-form-urlencoded"
|
||||
def encode(form)
|
||||
if multipart?(form)
|
||||
Multipart::Encoder.new(form)
|
||||
else
|
||||
Encoder.new(form)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Decoder
|
||||
module_function
|
||||
def decode(response)
|
||||
content_type = response.content_type.mime_type
|
||||
|
||||
def call(response, *)
|
||||
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
|
||||
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
|
||||
case content_type
|
||||
when "application/x-www-form-urlencoded"
|
||||
Decoder
|
||||
when "multipart/form-data"
|
||||
Multipart::Decoder.new(response)
|
||||
else
|
||||
raise Error, "invalid form mime type (#{content_type})"
|
||||
end
|
||||
end
|
||||
|
||||
def multipart?(data)
|
||||
data.any? do |_, v|
|
||||
Multipart::MULTIPART_VALUE_COND.call(v) ||
|
||||
(v.respond_to?(:to_ary) && v.to_ary.any?(&Multipart::MULTIPART_VALUE_COND)) ||
|
||||
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| Multipart::MULTIPART_VALUE_COND.call(e) })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def encode(form)
|
||||
Encoder.new(form)
|
||||
end
|
||||
|
||||
def decode(response)
|
||||
content_type = response.content_type.mime_type
|
||||
|
||||
raise HTTPX::Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
|
||||
|
||||
Decoder
|
||||
end
|
||||
end
|
||||
end
|
||||
|
17
lib/httpx/transcoder/multipart.rb
Normal file
17
lib/httpx/transcoder/multipart.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "multipart/encoder"
|
||||
require_relative "multipart/decoder"
|
||||
require_relative "multipart/part"
|
||||
require_relative "multipart/mime_type_detector"
|
||||
|
||||
module HTTPX::Transcoder
|
||||
module Multipart
|
||||
MULTIPART_VALUE_COND = lambda do |value|
|
||||
value.respond_to?(:read) ||
|
||||
(value.respond_to?(:to_hash) &&
|
||||
value.key?(:body) &&
|
||||
(value.key?(:filename) || value.key?(:content_type)))
|
||||
end
|
||||
end
|
||||
end
|
139
lib/httpx/transcoder/multipart/decoder.rb
Normal file
139
lib/httpx/transcoder/multipart/decoder.rb
Normal file
@ -0,0 +1,139 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "tempfile"
|
||||
require "delegate"
|
||||
|
||||
module HTTPX
|
||||
module Transcoder
|
||||
module Multipart
|
||||
class FilePart < SimpleDelegator
|
||||
attr_reader :original_filename, :content_type
|
||||
|
||||
def initialize(filename, content_type)
|
||||
@original_filename = filename
|
||||
@content_type = content_type
|
||||
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
||||
super(@file)
|
||||
end
|
||||
end
|
||||
|
||||
class Decoder
|
||||
include HTTPX::Utils
|
||||
|
||||
CRLF = "\r\n"
|
||||
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
||||
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
||||
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
||||
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
||||
WINDOW_SIZE = 2 << 14
|
||||
|
||||
def initialize(response)
|
||||
@boundary = begin
|
||||
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
||||
raise Error, "no boundary declared in content-type header" unless m
|
||||
|
||||
m.strip
|
||||
end
|
||||
@buffer = "".b
|
||||
@parts = {}
|
||||
@intermediate_boundary = "--#{@boundary}"
|
||||
@state = :idle
|
||||
end
|
||||
|
||||
def call(response, *)
|
||||
response.body.each do |chunk|
|
||||
@buffer << chunk
|
||||
|
||||
parse
|
||||
end
|
||||
|
||||
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
||||
|
||||
@parts
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse
|
||||
case @state
|
||||
when :idle
|
||||
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
||||
|
||||
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
||||
|
||||
@state = :part_header
|
||||
when :part_header
|
||||
idx = @buffer.index("#{CRLF}#{CRLF}")
|
||||
|
||||
# raise Error, "couldn't parse part headers" unless idx
|
||||
return unless idx
|
||||
|
||||
head = @buffer.byteslice(0..idx + 4 - 1)
|
||||
|
||||
@buffer = @buffer.byteslice(head.bytesize..-1)
|
||||
|
||||
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
||||
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
||||
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
||||
name.gsub!(/\\(.)/, "\\1")
|
||||
name
|
||||
else
|
||||
name = head[MULTIPART_CONTENT_ID, 1]
|
||||
end
|
||||
|
||||
filename = HTTPX::Utils.get_filename(head)
|
||||
|
||||
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
||||
|
||||
@current = name
|
||||
|
||||
@parts[name] = if filename
|
||||
FilePart.new(filename, content_type)
|
||||
else
|
||||
"".b
|
||||
end
|
||||
|
||||
@state = :part_body
|
||||
when :part_body
|
||||
part = @parts[@current]
|
||||
|
||||
body_separator = if part.is_a?(FilePart)
|
||||
"#{CRLF}#{CRLF}"
|
||||
else
|
||||
CRLF
|
||||
end
|
||||
idx = @buffer.index(body_separator)
|
||||
|
||||
if idx
|
||||
payload = @buffer.byteslice(0..idx - 1)
|
||||
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
||||
part << payload
|
||||
part.rewind if part.respond_to?(:rewind)
|
||||
@state = :parse_boundary
|
||||
else
|
||||
part << @buffer
|
||||
@buffer.clear
|
||||
end
|
||||
when :parse_boundary
|
||||
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
||||
|
||||
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
||||
|
||||
if @buffer == "--"
|
||||
@buffer.clear
|
||||
@state = :done
|
||||
return
|
||||
elsif @buffer.start_with?(CRLF)
|
||||
@buffer = @buffer.byteslice(2..-1)
|
||||
@state = :part_header
|
||||
else
|
||||
return
|
||||
end
|
||||
when :done
|
||||
raise Error, "parsing should have been over by now"
|
||||
end until @buffer.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX::Plugins
|
||||
module Multipart
|
||||
module HTTPX
|
||||
module Transcoder::Multipart
|
||||
class Encoder
|
||||
attr_reader :bytesize
|
||||
|
||||
@ -43,7 +43,7 @@ module HTTPX::Plugins
|
||||
def to_parts(form)
|
||||
@bytesize = 0
|
||||
params = form.each_with_object([]) do |(key, val), aux|
|
||||
Multipart.normalize_keys(key, val) do |k, v|
|
||||
Transcoder.normalize_keys(key, val, MULTIPART_VALUE_COND) do |k, v|
|
||||
next if v.nil?
|
||||
|
||||
value, content_type, filename = Part.call(v)
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Plugins::Multipart
|
||||
module Transcoder::Multipart
|
||||
module MimeTypeDetector
|
||||
module_function
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Plugins::Multipart
|
||||
module Transcoder::Multipart
|
||||
module Part
|
||||
module_function
|
||||
|
@ -9,8 +9,7 @@ class Bug_0_14_1_Test < Minitest::Test
|
||||
def test_multipart_can_have_arbitrary_content_type
|
||||
uri = "https://#{httpbin}/post"
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post(uri, form: {
|
||||
response = HTTPX.post(uri, form: {
|
||||
image: {
|
||||
content_type: "image/png",
|
||||
body: File.new(fixture_file_path),
|
||||
|
@ -9,8 +9,7 @@ class Bug_0_14_2_Test < Minitest::Test
|
||||
def test_multipart_can_have_arbitrary_filename
|
||||
uri = "https://#{httpbin}/post"
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post(uri, form: {
|
||||
response = HTTPX.post(uri, form: {
|
||||
image: {
|
||||
filename: "weird-al-jankovic",
|
||||
body: File.new(fixture_file_path),
|
||||
|
@ -23,7 +23,6 @@ module HTTPX
|
||||
| (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects
|
||||
| (:upgrade, ?options) -> Session
|
||||
| (:h2c, ?options) -> Session
|
||||
| (:multipart, ?options) -> Session
|
||||
| (:persistent, ?options) -> Plugins::sessionPersistent
|
||||
| (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
|
||||
| (:push_promise, ?options) -> Plugins::sessionPushPromise
|
||||
|
@ -3,11 +3,12 @@ module HTTPX::Transcoder
|
||||
|
||||
type form_nested_value = form_value | _ToAry[form_value] | _ToHash[string, form_value]
|
||||
|
||||
type urlencoded_input = Enumerable[[_ToS, form_nested_value]]
|
||||
type urlencoded_input = Enumerable[[_ToS, form_nested_value | Multipart::multipart_nested_value]]
|
||||
|
||||
module Form
|
||||
def self?.encode: (urlencoded_input form) -> Encoder
|
||||
def self?.encode: (urlencoded_input form) -> (Encoder | Multipart::Encoder)
|
||||
def self?.decode: (HTTPX::Response response) -> _Decoder
|
||||
def self?.multipart?: (form_nested_value | Multipart::multipart_nested_value data) -> bool
|
||||
|
||||
class Encoder
|
||||
extend Forwardable
|
||||
|
@ -1,5 +1,5 @@
|
||||
module HTTPX
|
||||
module Plugins
|
||||
module Transcoder
|
||||
module Multipart
|
||||
interface _MultipartInput
|
||||
def filename: () -> String
|
||||
@ -9,13 +9,6 @@ module HTTPX
|
||||
|
||||
MULTIPART_VALUE_COND: ^(_Reader | record_multipart_value value) -> bool
|
||||
|
||||
def self.load_dependencies: (singleton(Session)) -> void
|
||||
def self.configure: (*untyped) -> void
|
||||
def self?.encode: (untyped) -> (Encoder | Transcoder::Form::Encoder)
|
||||
def self?.decode: (HTTPX::Response response) -> Transcoder::_Decoder
|
||||
|
||||
def self?.normalize_keys: [U] (_ToS key, _ToAry[untyped] | _ToHash[_ToS, untyped] | untyped value) { (String, ?untyped) -> U } -> U
|
||||
|
||||
type multipart_value = string | Pathname | File | _Reader
|
||||
|
||||
type record_multipart_value = { content_type: String, filename: String, body: multipart_value } |
|
@ -14,8 +14,7 @@ class MultipartFilemagicTest < Minitest::Test
|
||||
|
||||
filemagic_spy = Spy.on(FileMagic, :open).and_call_through
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post("https://#{httpbin}/post", form: { image: File.new(fixture_file_path) })
|
||||
response = HTTPX.post("https://#{httpbin}/post", form: { image: File.new(fixture_file_path) })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
|
@ -14,8 +14,7 @@ class MultipartMarcelTest < Minitest::Test
|
||||
|
||||
marcel_spy = Spy.on(Marcel::MimeType, :for).and_call_through
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post("https://#{httpbin}/post", form: { image: File.new(fixture_file_path) })
|
||||
response = HTTPX.post("https://#{httpbin}/post", form: { image: File.new(fixture_file_path) })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
|
@ -14,8 +14,7 @@ class MultipartMimemagicTest < Minitest::Test
|
||||
|
||||
mimemagic_spy = Spy.on(MimeMagic, :by_magic).and_call_through
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post("https://#{httpbin}/post", form: { image: File.new(fixture_file_path) })
|
||||
response = HTTPX.post("https://#{httpbin}/post", form: { image: File.new(fixture_file_path) })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
|
@ -10,6 +10,7 @@ class HTTPTest < Minitest::Test
|
||||
include ChunkedGet
|
||||
include WithBody
|
||||
include WithChunkedBody
|
||||
include Multipart
|
||||
include Headers
|
||||
include ResponseBody
|
||||
include IO
|
||||
@ -25,7 +26,6 @@ class HTTPTest < Minitest::Test
|
||||
include Plugins::Compression
|
||||
include Plugins::H2C
|
||||
include Plugins::Retries
|
||||
include Plugins::Multipart
|
||||
include Plugins::Expect
|
||||
include Plugins::RateLimiter
|
||||
include Plugins::Stream
|
||||
|
@ -8,6 +8,7 @@ class HTTPSTest < Minitest::Test
|
||||
include Get
|
||||
include Head
|
||||
include WithBody
|
||||
include Multipart
|
||||
include Headers
|
||||
include ResponseBody
|
||||
include IO
|
||||
@ -25,7 +26,6 @@ class HTTPSTest < Minitest::Test
|
||||
include Plugins::Compression
|
||||
include Plugins::PushPromise if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
|
||||
include Plugins::Retries
|
||||
include Plugins::Multipart
|
||||
include Plugins::Expect
|
||||
include Plugins::RateLimiter
|
||||
include Plugins::Persistent
|
||||
|
243
test/support/requests/multipart.rb
Normal file
243
test/support/requests/multipart.rb
Normal file
@ -0,0 +1,243 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "http/form_data"
|
||||
|
||||
module Requests
|
||||
module Multipart
|
||||
%w[post put patch delete].each do |meth|
|
||||
define_method :"test_multipart_urlencoded_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { "foo" => "bar" })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "application/x-www-form-urlencoded")
|
||||
verify_uploaded(body, "form", "foo" => "bar")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_urlencoded_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { "q" => { "a" => "z" }, "a" => %w[1 2] })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "application/x-www-form-urlencoded")
|
||||
verify_uploaded(body, "form", "q[a]" => "z", "a[]" => %w[1 2])
|
||||
end
|
||||
|
||||
define_method :"test_multipart_repeated_field_urlencoded_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: [%w[foo bar1], %w[foo bar2]])
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "application/x-www-form-urlencoded")
|
||||
verify_uploaded(body, "form", "foo" => %w[bar1 bar2])
|
||||
end
|
||||
|
||||
define_method :"test_multipart_hash_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { metadata: { content_type: "application/json", body: JSON.dump({ a: 1 }) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
assert JSON.parse(body["form"]["metadata"], symbolize_names: true) == { a: 1 }
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_hash_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { q: { metadata: { content_type: "application/json", body: JSON.dump({ a: 1 }) } } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
assert JSON.parse(body["form"]["q[metadata]"], symbolize_names: true) == { a: 1 }
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_array_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { q: [{ content_type: "application/json", body: JSON.dump({ a: 1 }) }] })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
assert JSON.parse(body["form"]["q[]"], symbolize_names: true) == { a: 1 }
|
||||
end
|
||||
|
||||
define_method :"test_multipart_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { image: File.new(fixture_file_path) })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_file_repeated_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: [
|
||||
%w[foo bar1],
|
||||
["image1", File.new(fixture_file_path)],
|
||||
%w[foo bar2],
|
||||
["image2", File.new(fixture_file_path)],
|
||||
])
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded(body, "form", "foo" => %w[bar1 bar2])
|
||||
verify_uploaded_image(body, "image1", "image/jpeg")
|
||||
verify_uploaded_image(body, "image2", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { q: { image: File.new(fixture_file_path) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_ary_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { images: [File.new(fixture_file_path)] })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "images[]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_filename_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { image: { filename: "selfie", body: File.new(fixture_file_path) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
# TODO: find out how to check the filename given.
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_filename_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { q: { image: { filename: "selfie", body: File.new(fixture_file_path) } } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
# TODO: find out how to check the filename given.
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_filename_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { q: { image: File.new(fixture_file_path) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_pathname_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = Pathname.new(fixture_file_path)
|
||||
response = HTTPX.send(meth, uri, form: { image: file })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_pathname_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = Pathname.new(fixture_file_path)
|
||||
response = HTTPX.send(meth, uri, form: { q: { image: file } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_http_formdata_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = HTTP::FormData::File.new(fixture_file_path, content_type: "image/jpeg")
|
||||
response = HTTPX.send(meth, uri, form: { image: file })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_nested_http_formdata_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = HTTP::FormData::File.new(fixture_file_path, content_type: "image/jpeg")
|
||||
response = HTTPX.send(meth, uri, form: { q: { image: file } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_multipart_spoofed_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.send(meth, uri, form: { image: {
|
||||
content_type: "image/jpeg",
|
||||
filename: "selfie",
|
||||
body: "spoofpeg",
|
||||
} })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
# httpbin accepts the spoofed part, but it wipes our the content-type header
|
||||
verify_uploaded_image(body, "image", "spoofpeg", skip_verify_data: true)
|
||||
end
|
||||
end
|
||||
|
||||
# safety-check test only check if request is successfully rewinded
|
||||
def test_multipart_retry_file_post
|
||||
check_error = lambda { |response|
|
||||
(response.is_a?(HTTPX::ErrorResponse) && response.error.is_a?(HTTPX::TimeoutError)) || response.status == 405
|
||||
}
|
||||
uri = build_uri("/delay/4")
|
||||
retries_session = HTTPX.plugin(RequestInspector)
|
||||
.plugin(:retries, max_retries: 1, retry_on: check_error) # because CI...
|
||||
.with_timeout(request_timeout: 2)
|
||||
retries_response = retries_session.post(uri, retry_change_requests: true, form: { image: File.new(fixture_file_path) })
|
||||
assert check_error[retries_response], "expected #{retries_response} to be an error response"
|
||||
assert retries_session.calls == 1, "expect request to be retried 1 time (was #{retries_session.calls})"
|
||||
end
|
||||
|
||||
def test_multipart_response_decoder
|
||||
form_response = HTTPX::Response.new(
|
||||
HTTPX::Request.new("GET", "http://example.com"),
|
||||
200,
|
||||
"2.0",
|
||||
{ "content-type" => "multipart/form-data; boundary=90" }
|
||||
)
|
||||
form_response << [
|
||||
"--90\r\n",
|
||||
"Content-Disposition: form-data; name=\"text\"\r\n\r\n",
|
||||
"text default\r\n",
|
||||
"--90\r\n",
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n",
|
||||
"Content-Type: text/plain\r\n\r\n",
|
||||
"Content of a.txt.\r\n\r\n",
|
||||
"--90\r\n",
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\r\n",
|
||||
"Content-Type: text/html\r\n\r\n",
|
||||
"<!DOCTYPE html><title>Content of a.html.</title>\r\n\r\n",
|
||||
"--90--",
|
||||
].join
|
||||
form = form_response.form
|
||||
|
||||
begin
|
||||
assert form["text"] == "text default"
|
||||
assert form["file1"].original_filename == "a.txt"
|
||||
assert form["file1"].content_type == "text/plain"
|
||||
assert form["file1"].read == "Content of a.txt."
|
||||
|
||||
assert form["file2"].original_filename == "a.html"
|
||||
assert form["file2"].content_type == "text/html"
|
||||
assert form["file2"].read == "<!DOCTYPE html><title>Content of a.html.</title>"
|
||||
ensure
|
||||
form["file1"].close
|
||||
form["file1"].unlink
|
||||
form["file2"].close
|
||||
form["file2"].unlink
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,267 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "http/form_data"
|
||||
|
||||
module Requests
|
||||
module Plugins
|
||||
module Multipart
|
||||
%w[post put patch delete].each do |meth|
|
||||
define_method :"test_plugin_multipart_urlencoded_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { "foo" => "bar" })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "application/x-www-form-urlencoded")
|
||||
verify_uploaded(body, "form", "foo" => "bar")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_urlencoded_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { "q" => { "a" => "z" }, "a" => %w[1 2] })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "application/x-www-form-urlencoded")
|
||||
verify_uploaded(body, "form", "q[a]" => "z", "a[]" => %w[1 2])
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_repeated_field_urlencoded_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: [%w[foo bar1], %w[foo bar2]])
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "application/x-www-form-urlencoded")
|
||||
verify_uploaded(body, "form", "foo" => %w[bar1 bar2])
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_hash_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { metadata: { content_type: "application/json", body: JSON.dump({ a: 1 }) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
assert JSON.parse(body["form"]["metadata"], symbolize_names: true) == { a: 1 }
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_hash_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: { metadata: { content_type: "application/json", body: JSON.dump({ a: 1 }) } } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
assert JSON.parse(body["form"]["q[metadata]"], symbolize_names: true) == { a: 1 }
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_array_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: [{ content_type: "application/json", body: JSON.dump({ a: 1 }) }] })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
assert JSON.parse(body["form"]["q[]"], symbolize_names: true) == { a: 1 }
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { image: File.new(fixture_file_path) })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_file_repeated_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: [
|
||||
%w[foo bar1],
|
||||
["image1", File.new(fixture_file_path)],
|
||||
%w[foo bar2],
|
||||
["image2", File.new(fixture_file_path)],
|
||||
])
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded(body, "form", "foo" => %w[bar1 bar2])
|
||||
verify_uploaded_image(body, "image1", "image/jpeg")
|
||||
verify_uploaded_image(body, "image2", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: { image: File.new(fixture_file_path) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_ary_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { images: [File.new(fixture_file_path)] })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "images[]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_filename_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { image: { filename: "selfie", body: File.new(fixture_file_path) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
# TODO: find out how to check the filename given.
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_filename_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: { image: { filename: "selfie", body: File.new(fixture_file_path) } } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
# TODO: find out how to check the filename given.
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_filename_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: { image: File.new(fixture_file_path) } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_pathname_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = Pathname.new(fixture_file_path)
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { image: file })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_pathname_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = Pathname.new(fixture_file_path)
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: { image: file } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_http_formdata_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = HTTP::FormData::File.new(fixture_file_path, content_type: "image/jpeg")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { image: file })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "image", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_nested_http_formdata_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
file = HTTP::FormData::File.new(fixture_file_path, content_type: "image/jpeg")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { q: { image: file } })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
verify_uploaded_image(body, "q[image]", "image/jpeg")
|
||||
end
|
||||
|
||||
define_method :"test_plugin_multipart_spoofed_file_#{meth}" do
|
||||
uri = build_uri("/#{meth}")
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.send(meth, uri, form: { image: {
|
||||
content_type: "image/jpeg",
|
||||
filename: "selfie",
|
||||
body: "spoofpeg",
|
||||
} })
|
||||
verify_status(response, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body["headers"], "Content-Type", "multipart/form-data")
|
||||
# httpbin accepts the spoofed part, but it wipes our the content-type header
|
||||
verify_uploaded_image(body, "image", "spoofpeg", skip_verify_data: true)
|
||||
end
|
||||
end
|
||||
|
||||
# safety-check test only check if request is successfully rewinded
|
||||
def test_plugin_multipart_retry_file_post
|
||||
check_error = lambda { |response|
|
||||
(response.is_a?(HTTPX::ErrorResponse) && response.error.is_a?(HTTPX::TimeoutError)) || response.status == 405
|
||||
}
|
||||
uri = build_uri("/delay/4")
|
||||
retries_session = HTTPX.plugin(RequestInspector)
|
||||
.plugin(:retries, max_retries: 1, retry_on: check_error) # because CI...
|
||||
.with_timeout(request_timeout: 2)
|
||||
.plugin(:multipart)
|
||||
retries_response = retries_session.post(uri, retry_change_requests: true, form: { image: File.new(fixture_file_path) })
|
||||
assert check_error[retries_response], "expected #{retries_response} to be an error response"
|
||||
assert retries_session.calls == 1, "expect request to be retried 1 time (was #{retries_session.calls})"
|
||||
end
|
||||
|
||||
def test_plugin_multipart_response_decoder
|
||||
form_response = HTTPX.plugin(:multipart)
|
||||
.class.default_options
|
||||
.response_class
|
||||
.new(
|
||||
HTTPX::Request.new("GET", "http://example.com"),
|
||||
200,
|
||||
"2.0",
|
||||
{ "content-type" => "multipart/form-data; boundary=90" }
|
||||
)
|
||||
form_response << [
|
||||
"--90\r\n",
|
||||
"Content-Disposition: form-data; name=\"text\"\r\n\r\n",
|
||||
"text default\r\n",
|
||||
"--90\r\n",
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n",
|
||||
"Content-Type: text/plain\r\n\r\n",
|
||||
"Content of a.txt.\r\n\r\n",
|
||||
"--90\r\n",
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\r\n",
|
||||
"Content-Type: text/html\r\n\r\n",
|
||||
"<!DOCTYPE html><title>Content of a.html.</title>\r\n\r\n",
|
||||
"--90--",
|
||||
].join
|
||||
form = form_response.form
|
||||
|
||||
begin
|
||||
assert form["text"] == "text default"
|
||||
assert form["file1"].original_filename == "a.txt"
|
||||
assert form["file1"].content_type == "text/plain"
|
||||
assert form["file1"].read == "Content of a.txt."
|
||||
|
||||
assert form["file2"].original_filename == "a.html"
|
||||
assert form["file2"].content_type == "text/html"
|
||||
assert form["file2"].read == "<!DOCTYPE html><title>Content of a.html.</title>"
|
||||
ensure
|
||||
form["file1"].close
|
||||
form["file1"].unlink
|
||||
form["file2"].close
|
||||
form["file2"].unlink
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user