bblimke / webmock

Library for stubbing and setting expectations on HTTP requests in Ruby.
MIT License
3.96k stars 557 forks source link

Typhoeus return_message lost when creating a Webmock response #1057

Open thebravoman opened 4 months ago

thebravoman commented 4 months ago

Send request to "http://post_zip.pixel/2" This is invalid and typhoeus will return code:0 return_message: "Couldn't resolve host name"

This is lost when creating the webmock response

   78:           request_signature
   79:         end
   80: 
   81: 
   82:         def self.build_webmock_response(typhoeus_response)
=> 83:           webmock_response = WebMock::Response.new
   84:           webmock_response.status = [typhoeus_response.code, typhoeus_response.status_message]
   85:           webmock_response.body = typhoeus_response.body
   86:           webmock_response.headers = typhoeus_response.headers
   87:           webmock_response
(byebug) typhoeus_response
#<Typhoeus::Response:0x0000000112c8a920 @options={:httpauth_avail=>0, :total_time=>0.000744, :starttransfer_time=>0.0, :appconnect_time=>0.0, :pretransfer_time=>0.0, :connect_time=>0.0, :namelookup_time=>0.0, :redirect_time=>0.0, :effective_url=>"http://post_zip.pixel/2", :primary_ip=>"", :response_code=>0, :request_size=>0, :redirect_count=>0, :size_upload=>0.0, :size_download=>0.0, :speed_upload=>0.0, :speed_download=>0.0, :return_code=>:couldnt_resolve_host, :response_headers=>"", :response_body=>"", :debug_info=>#<Ethon::Easy::DebugInfo:0x0000000112c714c0 @messages=[]>}, @request=#<Typhoeus::Request:0x00000001220ed698 @base_url="http://post_zip.pixel/2", @original_options={:method=>"post", :timeout=>5}, @options={:method=>"post", :timeout=>5, :headers=>{"User-Agent"=>"RetreaverPinger/2.0 (+https://retreaver.com/bot)", "Expect"=>""}, :maxredirs=>50}, @on_complete=[#<Proc:0x00000001220ed418 /Users/kireto/axles/code/callpixels/lib/pinger_diversion.rb:213>], @__webmock_request_signature=#<WebMock::RequestSignature:0x00000001220f7558 @method=:post, @uri=#<Addressable::URI:0x4e46c URI:http://post_zip.pixel:80/2>, @body=nil, @headers={"User-Agent"=>"RetreaverPinger/2.0 (+https://retreaver.com/bot)", "Expect"=>""}, @__typed_vcr_request=#<struct VCR::Request method=:post, uri="http://post_zip.pixel/2", body="", headers={"User-Agent"=>["RetreaverPinger/2.0 (+https://retreaver.com/bot)"], "Expect"=>[""]}>>, @block_connection=false, @response=#<Typhoeus::Response:0x0000000112c8a920 ...>, @on_failure=[]>, @first_header_line=nil, @status_message=nil, @headers={}>
(byebug) typhoeus_response.status_message
nil
(byebug) typhoeus_response.return_message
"Couldn't resolve host name"
(byebug) 

Status_message is nil and the return_message is "Could't resolve host name" When the status is created it is done as webmock_response.status = [typhoeus_response.code, typhoeus_response.status_message]

From there on the actual problem for my case is that this gets saved in a vcr cassette as

- request:
    method: post
    uri: http://post_zip.pixel/2
    body:
      encoding: US-ASCII
      string: ''
    headers:
      User-Agent:
      - RetreaverPinger/2.0 (+https://retreaver.com/bot)
      Expect:
      - ''
  response:
    status:
      code: 0
      message:
    headers: {}
    body:
      encoding: ASCII-8BIT
      string: ''
  recorded_at: Fri, 03 May 2024 07:59:00 GMT

where the status code is 0 and the message is empty.

This leads to the following situation. The first time a request is made and there is no cassette typhoeus has return_message as "Could not resolve host" and we can test in our specs that the host was not reached and we've shown the user the error message.

The second time the spec is ran there is already a cassette. The cassette has status message as empty and the test no longer works as it does not load "could not resolve host" into the typhoeus return_message

Probably something along the lines of

        def self.build_webmock_response(typhoeus_response)
          webmock_response = WebMock::Response.new
          status_message = typhoeus_response.status_message
          status_message ||= typhoeus_response.return_message if typhoeus_response.code == 0

          webmock_response.status = [typhoeus_response.code, status_message]

could be useful, but it will mess up status_message with return_message and return message should probably be saved/loaded in another way

bblimke commented 4 months ago

@thebravoman Thank you for reporting this issue.

The return_message information is not based on the response code but on the return_code, which is not part of HTTP but rather an internal aspect of libcurl. For example, status code 0 is also returned on a timeout.

Please consider the following example:

irb(main):002> res = Typhoeus.get("http://post_zip.pixel/2")
=>
#<Typhoeus::Response:0x0000000129bb1410
...
irb(main):003> res.code
=> 0
irb(main):004> res.return_code
=> :couldnt_resolve_host
irb(main):005> res.return_message
=> "Couldn't resolve host name"
irb(main):006> Ethon::Curl.easy_strerror(:couldnt_resolve_host)
=> "Couldn't resolve host name"

WebMock::Response only contains HTTP attributes and doesn't carry any HTTP client-specific data, only pure HTTP response information like status code and status message.

The recorded response in the VCR YAML is correct since the response status message is nil:

irb(main):010> res.status_message
=> nil

We could possibly extend WebMock::Response to carry client_specific_response_attributes that would contain return_code and return_message. However, even then, VCR would not record these attributes as VCR also doesn't support any attributes other than pure HTTP data. Therefore, the solution is not going to work unless both WebMock and VCR support client-specific response attributes.