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/completion_executor.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, CompletionExecutor, 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, UnknownProcessorError

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:



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

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



442
443
444
445
446
447
# File 'lib/patient_http.rb', line 442

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



314
315
316
# File 'lib/patient_http.rb', line 314

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



253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/patient_http.rb', line 253

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



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/patient_http.rb', line 226

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



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

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

.handler_registered?Boolean

Check if a request handler is registered.

Returns:

  • (Boolean)


209
210
211
# File 'lib/patient_http.rb', line 209

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:



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/patient_http.rb', line 181

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)


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

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



304
305
306
# File 'lib/patient_http.rb', line 304

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



284
285
286
# File 'lib/patient_http.rb', line 284

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



294
295
296
# File 'lib/patient_http.rb', line 294

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



123
124
125
126
127
128
129
130
131
132
133
# File 'lib/patient_http.rb', line 123

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



148
149
150
151
152
153
154
155
156
# File 'lib/patient_http.rb', line 148

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:



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/patient_http.rb', line 396

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, processor: 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

  • processor (String, Symbol, nil) (defaults to: nil)

    name of the processor that should execute the request; handlers that support named processors route on this value

Returns:

  • (Object)

    return value from the registered request handler



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/patient_http.rb', line 336

def request(
  method,
  url,
  callback:,
  headers: nil,
  body: nil,
  json: nil,
  params: nil,
  timeout: nil,
  raise_error_responses: nil,
  callback_args: nil,
  preprocessors: nil,
  processor: nil
)
  request = Request.new(
    method,
    url,
    body: body,
    json: json,
    headers: headers,
    params: params,
    timeout: timeout,
    preprocessors: preprocessors,
    processor: processor
  )
  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:



378
379
380
# File 'lib/patient_http.rb', line 378

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)


418
419
420
421
422
423
424
# File 'lib/patient_http.rb', line 418

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.



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

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)


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

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



163
164
165
166
167
# File 'lib/patient_http.rb', line 163

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