Class: PatientHttp::RequestTask

Inherits:
Object
  • Object
show all
Includes:
TimeHelper
Defined in:
lib/patient_http/request_task.rb

Overview

A wrapper around Request that includes callback and job context for the Processor. This class allows HTTP requests to be enqueued and processed asynchronously, tracking their lifecycle and providing methods to handle success and error callbacks.

Constant Summary collapse

SENSITIVE_HEADERS =

Headers that are sensitive to origin and should be stripped on cross-origin redirects

%w[authorization cookie].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from TimeHelper

#monotonic_time, #wall_clock_time

Constructor Details

#initialize(request:, task_handler:, callback:, callback_args: {}, raise_error_responses: false, redirects: [], id: nil, default_max_redirects: 5) ⇒ RequestTask

Initializes a new RequestTask.

Parameters:

  • request (Request)

    The HTTP request to wrap.

  • task_handler (TaskHandler)

    The handler for job lifecycle operations.

  • callback (String, Class)

    Class name or class for the callback service.

  • callback_args (Hash) (defaults to: {})

    Callback arguments (with string keys) to include in Response/Error objects. These will be accessible via response.callback_args or error.callback_args.

  • raise_error_responses (Boolean) (defaults to: false)

    Whether to raise HttpError for non-2xx responses.

  • redirects (Array<String>) (defaults to: [])

    URLs visited during redirect chain.

  • id (String, nil) (defaults to: nil)

    Unique UUID for tracking the task. If nil, a new UUID will be generated.

  • default_max_redirects (Integer) (defaults to: 5)

    Fallback max_redirects when request doesn’t specify one.

Raises:

  • (ArgumentError)


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
# File 'lib/patient_http/request_task.rb', line 52

def initialize(
  request:,
  task_handler:,
  callback:,
  callback_args: {},
  raise_error_responses: false,
  redirects: [],
  id: nil,
  default_max_redirects: 5
)
  @id = id&.to_s || SecureRandom.uuid
  @request = request
  @task_handler = task_handler
  @callback = callback.is_a?(Class) ? callback.name : callback.to_s
  @callback_args = CallbackValidator.validate_callback_args(callback_args) || {}
  @raise_error_responses = raise_error_responses
  @redirects = redirects || []
  @default_max_redirects = default_max_redirects

  @enqueued_at = nil
  @started_at = nil
  @completed_at = nil
  @response = nil
  @error = nil

  raise ArgumentError, "request is required" unless @request
  raise ArgumentError, "task_handler is required" unless @task_handler
  raise ArgumentError, "callback is required" if @callback.nil? || @callback.empty?
  CallbackValidator.validate!(@callback)
end

Instance Attribute Details

#callbackString (readonly)

Returns Class name for the callback service.

Returns:

  • (String)

    Class name for the callback service



23
24
25
# File 'lib/patient_http/request_task.rb', line 23

def callback
  @callback
end

#callback_argsHash (readonly)

Returns Callback arguments to include in Response/Error objects (never nil, defaults to empty hash).

Returns:

  • (Hash)

    Callback arguments to include in Response/Error objects (never nil, defaults to empty hash)



26
27
28
# File 'lib/patient_http/request_task.rb', line 26

def callback_args
  @callback_args
end

#errorException? (readonly)

Returns The error, set on failure.

Returns:

  • (Exception, nil)

    The error, set on failure



38
39
40
# File 'lib/patient_http/request_task.rb', line 38

def error
  @error
end

#idString (readonly)

Returns Unique UUID for tracking the task.

Returns:

  • (String)

    Unique UUID for tracking the task



14
15
16
# File 'lib/patient_http/request_task.rb', line 14

def id
  @id
end

#raise_error_responsesBoolean (readonly)

Returns Whether to raise HttpError for non-2xx responses.

Returns:

  • (Boolean)

    Whether to raise HttpError for non-2xx responses



29
30
31
# File 'lib/patient_http/request_task.rb', line 29

def raise_error_responses
  @raise_error_responses
end

#redirectsArray<String> (readonly)

Returns URLs visited during redirect chain.

Returns:

  • (Array<String>)

    URLs visited during redirect chain



32
33
34
# File 'lib/patient_http/request_task.rb', line 32

def redirects
  @redirects
end

#requestRequest (readonly)

Returns The HTTP request details.

Returns:

  • (Request)

    The HTTP request details



17
18
19
# File 'lib/patient_http/request_task.rb', line 17

def request
  @request
end

#responseResponse? (readonly)

Returns The HTTP response, set on success.

Returns:

  • (Response, nil)

    The HTTP response, set on success



35
36
37
# File 'lib/patient_http/request_task.rb', line 35

def response
  @response
end

#task_handlerTaskHandler (readonly)

Returns The handler for job lifecycle operations.

Returns:

  • (TaskHandler)

    The handler for job lifecycle operations



20
21
22
# File 'lib/patient_http/request_task.rb', line 20

def task_handler
  @task_handler
end

Instance Method Details

#build_response(status:, headers:, body:) ⇒ Response

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build a Response object from async response data.

Parameters:

  • status (Integer)

    HTTP status code

  • headers (Hash)

    HTTP response headers

  • body (String, nil)

    HTTP response body

Returns:



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/patient_http/request_task.rb', line 255

def build_response(status:, headers:, body:)
  original_id = id.split("/").first

  Response.new(
    status: status,
    headers: headers,
    body: body,
    duration: duration,
    request_id: original_id,
    url: request.url,
    http_method: request.http_method,
    callback_args: @callback_args,
    redirects: @redirects
  )
end

#completed!(response) ⇒ void

This method returns an undefined value.

Called with the HTTP response on a completed request. Note that the response may represent an HTTP error (4xx or 5xx status).

Parameters:

  • response (Response)

    the HTTP response



143
144
145
146
147
148
# File 'lib/patient_http/request_task.rb', line 143

def completed!(response)
  @completed_at = monotonic_time
  @response = response

  @task_handler.on_complete(response, @callback)
end

#completed_atTime?

Returns the wall clock time when the task was completed.

Returns:

  • (Time, nil)

    The completed time or nil if not completed.



112
113
114
# File 'lib/patient_http/request_task.rb', line 112

def completed_at
  wall_clock_time(@completed_at) if @completed_at
end

#durationFloat?

Execution duration in seconds.

Returns:

  • (Float, nil)

    duration or nil if not started yet.



126
127
128
129
130
# File 'lib/patient_http/request_task.rb', line 126

def duration
  return nil unless @started_at

  ((@completed_at || monotonic_time) - @started_at).round(9)
end

#enqueued!void

This method returns an undefined value.

Mark task as enqueued



85
86
87
# File 'lib/patient_http/request_task.rb', line 85

def enqueued!
  @enqueued_at = monotonic_time
end

#enqueued_atTime?

Returns the wall clock time when the task was enqueued.

Returns:

  • (Time, nil)

    The enqueued time or nil if not enqueued.



98
99
100
# File 'lib/patient_http/request_task.rb', line 98

def enqueued_at
  wall_clock_time(@enqueued_at) if @enqueued_at
end

#enqueued_durationFloat?

Enqueued duration in seconds.

Returns:

  • (Float, nil)

    duration or nil if not enqueued yet.



118
119
120
121
122
# File 'lib/patient_http/request_task.rb', line 118

def enqueued_duration
  return nil unless @enqueued_at

  (@started_at || monotonic_time) - @enqueued_at
end

#error!(exception) ⇒ void

This method returns an undefined value.

Called with the HTTP error on a failed request.

Parameters:

  • exception (Exception)

    the error that occurred



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/patient_http/request_task.rb', line 154

def error!(exception)
  @completed_at = monotonic_time
  @error = exception

  wrapped_error = exception
  unless wrapped_error.is_a?(Error)
    wrapped_error = RequestError.from_exception(
      exception,
      request_id: @id,
      duration: duration,
      url: request.url,
      http_method: request.http_method,
      callback_args: @callback_args
    )
  end

  @task_handler.on_error(wrapped_error, @callback)
end

#error?Boolean

Return true if an error was raised during the request.

Returns:

  • (Boolean)


184
185
186
# File 'lib/patient_http/request_task.rb', line 184

def error?
  !@error.nil?
end

#max_redirectsInteger

Returns the maximum number of redirects to follow. Uses the request’s max_redirects if set, otherwise falls back to the default.

Returns:

  • (Integer)

    maximum number of redirects



192
193
194
# File 'lib/patient_http/request_task.rb', line 192

def max_redirects
  request.max_redirects || @default_max_redirects
end

#original_idString

Get the id of the first request task before any redirects. This is useful for tracking the overall request across multiple redirect tasks.

Returns:

  • (String)

    the original request id



275
276
277
# File 'lib/patient_http/request_task.rb', line 275

def original_id
  id.split("/").first
end

#redirect_task(location:, status:) ⇒ RequestTask

Create a new RequestTask for following a redirect.

Parameters:

  • location (String)

    The redirect URL from the Location header

  • status (Integer)

    The HTTP status code of the redirect response

Returns:

  • (RequestTask)

    A new task configured for the redirect



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/patient_http/request_task.rb', line 201

def redirect_task(location:, status:)
  # Determine the HTTP method and body for the redirect
  # 301, 302, 303: Convert to GET (no body) - standard browser behavior
  # 307, 308: Preserve original method and body
  if [301, 302, 303].include?(status)
    redirect_method = :get
    redirect_body = nil
  else
    redirect_method = request.http_method
    redirect_body = request.body
  end

  # Resolve the redirect URL (handle relative URLs)
  redirect_url = resolve_redirect_url(location)

  # Strip sensitive headers on cross-origin redirects to prevent credential leakage
  redirect_headers = if cross_origin?(request.url, redirect_url)
    request.headers.except(*SENSITIVE_HEADERS)
  else
    request.headers
  end

  # Create a new request for the redirect
  redirect_request = Request.new(
    redirect_method,
    redirect_url,
    headers: redirect_headers,
    body: redirect_body,
    timeout: request.timeout,
    max_redirects: request.max_redirects
  )

  redirect_task_id = "#{id.split("/").first}/#{@redirects.size + 2}"

  # Create the new task with updated redirects chain
  self.class.new(
    request: redirect_request,
    task_handler: @task_handler,
    callback: @callback,
    callback_args: @callback_args,
    raise_error_responses: @raise_error_responses,
    redirects: @redirects + [request.url],
    id: redirect_task_id,
    default_max_redirects: @default_max_redirects
  )
end

#retryString

Re-enqueue the original job via the task handler.

Returns:

  • (String)

    job ID



134
135
136
# File 'lib/patient_http/request_task.rb', line 134

def retry
  @task_handler.retry
end

#started!void

This method returns an undefined value.

Mark task as started



91
92
93
# File 'lib/patient_http/request_task.rb', line 91

def started!
  @started_at = monotonic_time
end

#started_atTime?

Returns the wall clock time when the task was started.

Returns:

  • (Time, nil)

    The started time or nil if not started.



105
106
107
# File 'lib/patient_http/request_task.rb', line 105

def started_at
  wall_clock_time(@started_at) if @started_at
end

#success?Boolean

Return true if the task successfully received a response from the server. Note that the response may represent an HTTP error (4xx or 5xx status).

Returns:

  • (Boolean)


177
178
179
# File 'lib/patient_http/request_task.rb', line 177

def success?
  !@response.nil?
end