Module: PatientHttp

Defined in:
lib/patient_http.rb,
lib/patient_http/error.rb,
lib/patient_http/client.rb,
lib/patient_http/payload.rb,
lib/patient_http/request.rb,
lib/patient_http/response.rb,
lib/patient_http/encryptor.rb,
lib/patient_http/processor.rb,
lib/patient_http/http_error.rb,
lib/patient_http/client_pool.rb,
lib/patient_http/time_helper.rb,
lib/patient_http/class_helper.rb,
lib/patient_http/http_headers.rb,
lib/patient_http/rails/engine.rb,
lib/patient_http/request_task.rb,
lib/patient_http/task_handler.rb,
lib/patient_http/callback_args.rb,
lib/patient_http/configuration.rb,
lib/patient_http/payload_store.rb,
lib/patient_http/request_error.rb,
lib/patient_http/redirect_error.rb,
lib/patient_http/request_helper.rb,
lib/patient_http/secret_manager.rb,
lib/patient_http/redirect_helper.rb,
lib/patient_http/response_reader.rb,
lib/patient_http/external_storage.rb,
lib/patient_http/outgoing_request.rb,
lib/patient_http/request_preparer.rb,
lib/patient_http/request_template.rb,
lib/patient_http/secret_reference.rb,
lib/patient_http/lifecycle_manager.rb,
lib/patient_http/callback_validator.rb,
lib/patient_http/payload_store/base.rb,
lib/patient_http/processor_observer.rb,
lib/patient_http/inline_task_handler.rb,
lib/patient_http/synchronous_executor.rb,
lib/patient_http/payload_store/s3_store.rb,
lib/patient_http/payload_store/file_store.rb,
lib/patient_http/payload_store/redis_store.rb,
lib/patient_http/payload_store/active_record_store.rb

Overview

Generic async HTTP connection pool for Ruby applications.

This module provides:

  • Async HTTP request processing using Ruby's Fiber scheduler
  • Connection pooling with HTTP/2 support
  • Configurable timeouts, retries, and proxy support
  • Error handling with typed errors

This module can be used standalone or integrated with job systems like Sidekiq via adapters.

Defined Under Namespace

Modules: CallbackValidator, ClassHelper, PayloadStore, Rails, RedirectHelper, RequestHelper, TimeHelper Classes: CallbackArgs, Client, ClientError, ClientPool, Configuration, Encryptor, Error, ExternalStorage, HttpError, HttpHeaders, InlineTaskHandler, LifecycleManager, MaxCapacityError, NotRunningError, OutgoingRequest, Payload, Processor, ProcessorObserver, RecursiveRedirectError, RedirectError, Request, RequestError, RequestPreparer, RequestTask, RequestTemplate, Response, ResponseReader, ResponseTooLargeError, SecretManager, SecretReference, ServerError, SynchronousExecutor, TaskHandler, TooManyRedirectsError

Constant Summary collapse

FOLLOWABLE_REDIRECT_STATUSES =

HTTP redirect status codes that should be followed

[301, 302, 303, 307, 308].freeze
VERSION =
File.read(File.join(__dir__, "../VERSION")).strip

Class Method Summary collapse

Class Method Details

.default_configurationConfiguration?

The default configuration used for inline execution when none is provided. Job-system integration gems should set this at the end of their configure step so that module-level secrets registered with register_secret are applied to the configuration the processor runs with.

Returns:



421
422
423
# File 'lib/patient_http.rb', line 421

def default_configuration
  @config_mutex.synchronize { @default_configuration }
end

.default_configuration=(config) ⇒ void

This method returns an undefined value.

Set the default configuration. Any secrets registered with register_secret are applied to it; the module-level registry is retained, so re-assigning a new configuration re-applies the same secrets.

Parameters:

  • config (Configuration, nil)

    the configuration to use as the default



431
432
433
434
435
436
# File 'lib/patient_http.rb', line 431

def default_configuration=(config)
  @config_mutex.synchronize do
    @default_configuration = config
    apply_module_secrets(config) if config
  end
end

.delete(uri, callback:, **kwargs) ⇒ Object

Enqueues an HTTP DELETE request.

Parameters:

  • uri (String)

    absolute URL

  • callback (Class, String)

    callback class to handle the response

  • kwargs (Hash)

    forwarded to request

Returns:

  • (Object)

    return value from the registered request handler



307
308
309
# File 'lib/patient_http.rb', line 307

def delete(uri, callback:, **kwargs)
  request(:delete, uri, callback: callback, **kwargs)
end

.execute(request:, callback:, callback_args: nil, raise_error_responses: nil) ⇒ Object

Executes the registered request handler with the given request parameters.

Parameters:

  • request (Request)

    the HTTP request to handle

  • callback (Class, String)

    the callback class or name

  • callback_args (Hash, nil) (defaults to: nil)

    JSON-compatible callback arguments

  • raise_error_responses (Boolean, nil) (defaults to: nil)

    when true, non-success responses are reported as errors

Returns:

  • (Object)

    return value from the registered request handler

Raises:

  • (RuntimeError)

    if no handler is registered



246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/patient_http.rb', line 246

def execute(request:, callback:, callback_args: nil, raise_error_responses: nil)
  handler = @handler_mutex.synchronize { @handler }

  unless handler
    raise "No request handler registered; you must register a PatientHttp handler before executing requests"
  end

  handler.call(
    request: request,
    callback: callback,
    callback_args: callback_args,
    raise_error_responses: raise_error_responses
  )
end

.execute_inline(request:, callback:, callback_args: nil, raise_error_responses: nil, config: nil) ⇒ String

Executes a request inline (synchronously, in-process) through SynchronousExecutor, invoking the callback with the response or error before returning.

Parameters:

  • request (Request)

    the HTTP request to execute

  • callback (Class, String)

    the callback class or name

  • callback_args (Hash, nil) (defaults to: nil)

    JSON-compatible callback arguments

  • raise_error_responses (Boolean, nil) (defaults to: nil)

    when true, non-success responses are reported as errors; defaults to the configuration's setting

  • config (Configuration, nil) (defaults to: nil)

    configuration to execute the request against. Defaults to default_configuration, or a lazily created configuration that includes any secrets registered with register_secret.

Returns:

  • (String)

    the request id



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/patient_http.rb', line 219

def execute_inline(request:, callback:, callback_args: nil, raise_error_responses: nil, config: nil)
  config ||= default_configuration || inline_configuration
  raise_error_responses = config.raise_error_responses if raise_error_responses.nil?

  task = RequestTask.new(
    request: request,
    task_handler: InlineTaskHandler.new,
    callback: callback,
    callback_args: callback_args,
    raise_error_responses: raise_error_responses,
    default_max_redirects: config.max_redirects
  )

  SynchronousExecutor.new(task, config: config).call

  task.id
end

.get(uri, callback:, **kwargs) ⇒ Object

Enqueues an HTTP GET request.

Parameters:

  • uri (String)

    absolute URL

  • callback (Class, String)

    callback class to handle the response

  • kwargs (Hash)

    forwarded to request

Returns:

  • (Object)

    return value from the registered request handler



267
268
269
# File 'lib/patient_http.rb', line 267

def get(uri, callback:, **kwargs)
  request(:get, uri, callback: callback, **kwargs)
end

.handler_registered?Boolean

Check if a request handler is registered.

Returns:

  • (Boolean)


202
203
204
# File 'lib/patient_http.rb', line 202

def handler_registered?
  @handler_mutex.synchronize { !@handler.nil? }
end

.inline!(config: nil) ⇒ void

This method returns an undefined value.

Registers a request handler that executes requests inline (synchronously, in-process) instead of dispatching them to a job system.

This is intended for consoles, tests, and development environments where no job-system integration gem is configured. Each request runs through SynchronousExecutor and the callback is invoked on the calling thread before the handler returns.

Parameters:



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/patient_http.rb', line 174

def inline!(config: nil)
  handler = lambda do |request:, callback:, callback_args: nil, raise_error_responses: nil|
    execute_inline(
      request: request,
      callback: callback,
      callback_args: callback_args,
      raise_error_responses: raise_error_responses,
      config: config
    )
  end

  @handler_mutex.synchronize do
    register_handler(handler)
    @inline_handler = handler
  end
end

.inline?Boolean

Check if the currently registered handler is the inline handler registered by inline!.

Returns:

  • (Boolean)


195
196
197
# File 'lib/patient_http.rb', line 195

def inline?
  @handler_mutex.synchronize { !@handler.nil? && @handler.equal?(@inline_handler) }
end

.patch(uri, callback:, **kwargs) ⇒ Object

Enqueues an HTTP PATCH request.

Parameters:

  • uri (String)

    absolute URL

  • callback (Class, String)

    callback class to handle the response

  • kwargs (Hash)

    forwarded to request

Returns:

  • (Object)

    return value from the registered request handler



297
298
299
# File 'lib/patient_http.rb', line 297

def patch(uri, callback:, **kwargs)
  request(:patch, uri, callback: callback, **kwargs)
end

.post(uri, callback:, **kwargs) ⇒ Object

Enqueues an HTTP POST request.

Parameters:

  • uri (String)

    absolute URL

  • callback (Class, String)

    callback class to handle the response

  • kwargs (Hash)

    forwarded to request

Returns:

  • (Object)

    return value from the registered request handler



277
278
279
# File 'lib/patient_http.rb', line 277

def post(uri, callback:, **kwargs)
  request(:post, uri, callback: callback, **kwargs)
end

.put(uri, callback:, **kwargs) ⇒ Object

Enqueues an HTTP PUT request.

Parameters:

  • uri (String)

    absolute URL

  • callback (Class, String)

    callback class to handle the response

  • kwargs (Hash)

    forwarded to request

Returns:

  • (Object)

    return value from the registered request handler



287
288
289
# File 'lib/patient_http.rb', line 287

def put(uri, callback:, **kwargs)
  request(:put, uri, callback: callback, **kwargs)
end

.register_handler(callable = nil) {|request, callback, callback_args, raise_error_responses| ... } ⇒ #call

Registers a request handler that will be called to process each request. The handler must be a callable object (responds to call) or a block.

The handler will receive keyword arguments: request, callback, callback_args, and raise_error_responses. It should return the request id for the enqueued request.

Parameters:

  • callable (#call, nil) (defaults to: nil)

    A callable object that will handle requests.

Yields:

  • (request, callback, callback_args, raise_error_responses)

    If a block is given, it will be used as the request handler

Returns:

  • (#call)

    the registered handler

Raises:

  • (ArgumentError)

    if neither a callable nor a block is provided, or if both are provided

  • (ArgumentError)

    if the provided callable does not respond to call

  • (ArgumentError)

    if the handler does not support the required keyword arguments



116
117
118
119
120
121
122
123
124
125
126
# File 'lib/patient_http.rb', line 116

def register_handler(callable = nil, &block)
  raise ArgumentError.new("Must provide a callable object or a block") unless callable || block_given?
  raise ArgumentError.new("Cannot provide both a callable object and a block") if callable && block_given?

  handler = callable || block
  raise ArgumentError.new("Handler must be a callable object or a block") unless handler.respond_to?(:call)

  validate_handler_parameters!(handler)

  @handler_mutex.synchronize { @handler = handler }
end

.register_handler!(callable = nil) {|request, callback, callback_args, raise_error_responses| ... } ⇒ #call

Registers a request handler, raising an error if one is already registered.

This is a safer alternative to register_handler that prevents accidental double-registration.

Parameters:

  • callable (#call, nil) (defaults to: nil)

    A callable object that will handle requests.

Yields:

  • (request, callback, callback_args, raise_error_responses)

    If a block is given, it will be used as the request handler

Returns:

  • (#call)

    the registered handler

Raises:

  • (RuntimeError)

    if a handler is already registered

  • (ArgumentError)

    if neither a callable nor a block is provided, or if both are provided

  • (ArgumentError)

    if the provided callable does not respond to call

  • (ArgumentError)

    if the handler does not support the required keyword arguments



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

def register_handler!(callable = nil, &block)
  @handler_mutex.synchronize do
    if @handler
      raise "A PatientHttp handler is already registered. Unregister the existing handler before registering a new one."
    end

    register_handler(callable, &block)
  end
end

.register_secret(name, value = nil) {|name| ... } ⇒ void

This method returns an undefined value.

Register a named secret at the module level, independent of any configuration.

Secrets registered here are applied to the default_configuration (immediately if one is already set, or when one is set later) and to the configuration used for inline execution. This makes boot order irrelevant: application code can register secrets before or after the job-system integration gem configures the processor.

Parameters:

  • name (String, Symbol)

    the secret name

  • value (Object, nil) (defaults to: nil)

    the secret value (omit when providing a block)

Yields:

  • (name)

    a block that returns the secret value (omit when providing a value)

Raises:

  • (ArgumentError)

    if neither or both of value and block are provided

See Also:



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/patient_http.rb', line 385

def register_secret(name, value = nil, &block)
  if value.nil? && block.nil?
    raise ArgumentError.new("register_secret requires a value or a block")
  end

  if !value.nil? && block
    raise ArgumentError.new("register_secret accepts either a value or a block, not both")
  end

  @config_mutex.synchronize do
    secret_value = block || value
    @module_secrets[name.to_s] = secret_value
    @default_configuration&.register_secret(name, secret_value)
    @inline_configuration&.register_secret(name, secret_value)
  end
end

.request(method, url, callback:, headers: nil, body: nil, json: nil, params: nil, timeout: nil, raise_error_responses: nil, callback_args: nil, preprocessors: nil) ⇒ Object

Builds and dispatches an HTTP request.

Parameters:

  • method (Symbol)

    HTTP method (:get, :post, :put, :patch, :delete)

  • url (String)

    absolute URL

  • callback (Class, String)

    callback class to handle the response

  • headers (Hash, nil) (defaults to: nil)

    request headers

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

    raw request body

  • json (Hash, Array, nil) (defaults to: nil)

    JSON payload encoded by the request layer

  • params (Hash, nil) (defaults to: nil)

    query parameters

  • timeout (Numeric, nil) (defaults to: nil)

    timeout in seconds for this request

  • raise_error_responses (Boolean, nil) (defaults to: nil)

    when true, non-success responses are reported as errors

  • callback_args (Hash, nil) (defaults to: nil)

    JSON-compatible callback arguments

  • preprocessors (String, Symbol, Array<String, Symbol>, nil) (defaults to: nil)

    names of preprocessors registered on the configuration to apply to the request when it is sent

Returns:

  • (Object)

    return value from the registered request handler



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/patient_http.rb', line 327

def request(
  method,
  url,
  callback:,
  headers: nil,
  body: nil,
  json: nil,
  params: nil,
  timeout: nil,
  raise_error_responses: nil,
  callback_args: nil,
  preprocessors: nil
)
  request = Request.new(
    method,
    url,
    body: body,
    json: json,
    headers: headers,
    params: params,
    timeout: timeout,
    preprocessors: preprocessors
  )
  execute(
    request: request,
    callback: callback,
    callback_args: callback_args,
    raise_error_responses: raise_error_responses
  )
end

.secret(name) ⇒ SecretReference

Build a reference to a named secret for use as a sensitive header or query parameter value when building a request.

The reference holds only the secret's name; the value is resolved on the processor side at send time using the secrets registered on the configuration.

Parameters:

  • name (String, Symbol)

    the name of the secret to reference

Returns:

See Also:



367
368
369
# File 'lib/patient_http.rb', line 367

def secret(name)
  SecretReference.new(name)
end

.secret_registered?(name) ⇒ Boolean

Check if a secret name is registered, either at the module level via register_secret or on the default_configuration.

Parameters:

  • name (String, Symbol)

    the secret name

Returns:

  • (Boolean)


407
408
409
410
411
412
413
# File 'lib/patient_http.rb', line 407

def secret_registered?(name)
  @config_mutex.synchronize do
    return true if @module_secrets.include?(name.to_s)

    !@default_configuration.nil? && @default_configuration.secret_manager.include?(name)
  end
end

.testing=(value) ⇒ Object

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.

Set testing mode.



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

def testing=(value)
  @testing = !!value
end

.testing?Boolean

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.

Check if running in testing mode.

Returns:

  • (Boolean)


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

def testing?
  @testing
end

.unregister_handler(handler = nil) ⇒ void

This method returns an undefined value.

Unregisters the current request handler.

Parameters:

  • handler (#call, nil) (defaults to: nil)

    If provided, only unregisters if the given handler matches the current handler



156
157
158
159
160
# File 'lib/patient_http.rb', line 156

def unregister_handler(handler = nil)
  @handler_mutex.synchronize do
    @handler = nil if @handler == handler || handler.nil?
  end
end