Class: PatientHttp::SynchronousExecutor

Inherits:
Object
  • Object
show all
Includes:
RedirectHelper
Defined in:
lib/patient_http/synchronous_executor.rb

Overview

Handles synchronous/inline execution of HTTP requests.

Used for testing or when synchronous execution is needed. Accepts configuration and optional callback hooks so it has no dependency on any module-level singleton state.

Instance Method Summary collapse

Constructor Details

#initialize(task, config:, on_complete: nil, on_error: nil) ⇒ SynchronousExecutor

Returns a new instance of SynchronousExecutor.

Parameters:

  • task (RequestTask)

    the request task to execute

  • config (Configuration)

    the pool configuration

  • on_complete (Proc, nil) (defaults to: nil)

    hook called with response on success

  • on_error (Proc, nil) (defaults to: nil)

    hook called with error on failure



16
17
18
19
20
21
22
23
# File 'lib/patient_http/synchronous_executor.rb', line 16

def initialize(task, config:, on_complete: nil, on_error: nil)
  @task = task
  @config = config
  @on_complete = on_complete
  @on_error = on_error
  @proxy_client = nil
  @request_preparer = RequestPreparer.new(config)
end

Instance Method Details

#callvoid

This method returns an undefined value.

Execute the request synchronously.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/patient_http/synchronous_executor.rb', line 27

def call
  Async do
    start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)

    begin
      http_client = nil
      response_data = nil
      redirect_error = nil

      loop do
        http_client&.close
        @proxy_client&.close
        @proxy_client = nil
        outgoing = @request_preparer.prepare(@task.request, @task.id)
        http_client = create_http_client(outgoing.url)
        timeout = @task.request.timeout || @config.request_timeout

        response_data = Async::Task.current.with_timeout(timeout) do
          headers = outgoing.headers.to_h
          body = Protocol::HTTP::Body::Buffered.wrap([@task.request.body.to_s]) if @task.request.body

          endpoint = Async::HTTP::Endpoint.parse(outgoing.url)
          endpoint = configure_endpoint(endpoint) if @config.connection_timeout

          verb = @task.request.http_method.to_s.upcase
          options = {
            headers: headers,
            body: body,
            scheme: endpoint.scheme,
            authority: endpoint.authority
          }

          request = Protocol::HTTP::Request[verb, endpoint.path, **options]
          async_response = http_client.call(request)
          # Note: headers that appear multiple times (e.g. set-cookie) are
          # flattened to a single joined string value.
          headers_hash = async_response.headers.to_h.transform_values(&:to_s)

          body_content = read_response_body(async_response, headers_hash)

          {
            status: async_response.status,
            headers: headers_hash,
            body: body_content
          }
        end

        # Check for redirect
        break unless should_follow_redirect?(@task, response_data)

        # Note: a `return` here would raise LocalJumpError since this block
        # runs on a reactor fiber, so break out and handle the error below.
        redirect_error = check_redirect_error(@task, response_data)
        break if redirect_error

        location = response_data[:headers]["location"]
        @task = @task.redirect_task(location: location, status: response_data[:status])
      end

      if redirect_error
        invoke_callback(redirect_error, :error)
      else
        end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        duration = end_time - start_time

        response = Response.new(
          status: response_data[:status],
          headers: response_data[:headers],
          body: response_data[:body],
          duration: duration,
          request_id: @task.original_id,
          url: @task.request.url,
          http_method: @task.request.http_method,
          callback_args: @task.callback_args,
          redirects: @task.redirects
        )

        if @task.raise_error_responses && !response.success?
          http_error = HttpError.new(response)
          invoke_callback(http_error, :error)
        else
          invoke_callback(response, :response)
        end
      end
    rescue => e
      end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      duration = end_time - start_time

      error = RequestError.from_exception(
        e,
        request_id: @task.id,
        duration: duration,
        url: @task.request.url,
        http_method: @task.request.http_method,
        callback_args: @task.callback_args
      )
      invoke_callback(error, :error)
    ensure
      http_client&.close
      @proxy_client&.close
      @proxy_client = nil
    end
  end
end