Class: ReactOnRailsPro::RendererHttpClient

Inherits:
Object
  • Object
show all
Defined in:
lib/react_on_rails_pro/renderer_http_client.rb,
sig/react_on_rails_pro/renderer_http_client.rbs

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: BufferedResponseBody, ConnectTimeoutWrapper, ConnectionError, Error, HTTPError, MultipartBody, MultipartFileBody, PersistentThreadClient, Response, TimeoutError

Constant Summary collapse

CONNECTION_ERRORS =

Returns:

  • (Array[Class])
[
  SocketError,
  IOError,
  Errno::ECONNRESET,
  Errno::ECONNREFUSED,
  Errno::EHOSTUNREACH,
  Errno::ENETUNREACH,
  Errno::EPIPE,
  Errno::ETIMEDOUT,
  Protocol::HTTP::RefusedError,
  # Treat HTTP/2 stream resets as transport failures because the renderer can
  # abort streams without a usable HTTP response for Request/StreamRequest.
  Protocol::HTTP2::StreamError
].freeze
SCHEDULER_CLIENTS_KEY =

Per-scheduler storage for persistent HTTP clients. When an outer Fiber.scheduler exists BEFORE we enter Sync {}, clients are stored on the scheduler object using this instance variable key. This enables connection reuse across requests within the same long-lived scheduler context (e.g., Falcon, Puma with async scheduler). The hash maps origin URLs to Async::HTTP::Client instances.

IMPORTANT: We only use persistent mode when a scheduler already exists before execute_request enters Sync {}. If Sync {} creates an ephemeral scheduler, we use the ephemeral client path to ensure proper cleanup when the block exits.

Returns:

  • (Symbol)
:@__ror_pro_http_clients__
CLIENT_GENERATION_MUTEX =
Mutex.new

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(origin:, pool_size:, connect_timeout:, read_timeout:, force_http2: true) ⇒ RendererHttpClient

Returns a new instance of RendererHttpClient.



563
564
565
566
567
568
569
570
571
572
573
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 563

def initialize(origin:, pool_size:, connect_timeout:, read_timeout:, force_http2: true)
  @origin = origin
  @pool_size = pool_size
  @connect_timeout = connect_timeout
  @read_timeout = read_timeout
  @force_h2c = force_http2 && URI.parse(origin).scheme == "http"
  @thread_clients = {}.compare_by_identity
  @thread_clients_mutex = Mutex.new
  @closed = false
  @closed_mutex = Mutex.new
end

Class Method Details

.build_form_body(form) ⇒ [Array[[String, String]], MultipartBody | String]

Parameters:

  • form (Hash[untyped, untyped])

Returns:



489
490
491
492
493
494
495
496
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 489

def build_form_body(form)
  return build_multipart_body(form) if form.any? { |_name, value| file_part?(value) }

  [
    [["content-type", "application/x-www-form-urlencoded"]],
    URI.encode_www_form(flatten_url_encoded_form(form))
  ]
end

.build_multipart_body(form, boundary: SecureRandom.hex(24)) ⇒ Object

Raises:

  • (ArgumentError)


472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 472

def build_multipart_body(form, boundary: SecureRandom.hex(24))
  raise ArgumentError, "boundary must not contain '--'" if boundary.include?("--")

  body = MultipartBody.new

  form.each do |name, value|
    append_multipart_value(body, boundary, name, value)
  end

  body << "--#{boundary}--\r\n"

  [
    [["content-type", "multipart/form-data; boundary=#{boundary}"]],
    body
  ]
end

.bump_client_generationInteger

Returns:

  • (Integer)


466
467
468
469
470
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 466

def bump_client_generation
  CLIENT_GENERATION_MUTEX.synchronize do
    @client_generation = (@client_generation || 0) + 1
  end
end

.client_generationInteger

Returns:

  • (Integer)


460
461
462
463
464
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 460

def client_generation
  CLIENT_GENERATION_MUTEX.synchronize do
    @client_generation ||= 0
  end
end

.get(url, connect_timeout:, read_timeout:) ⇒ Response

Parameters:

  • url (String)
  • connect_timeout: (Numeric, nil)
  • read_timeout: (Numeric, nil)

Returns:



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 435

def get(url, connect_timeout:, read_timeout:)
  origin, path = split_url(url)

  client = new(
    origin:,
    pool_size: 1,
    connect_timeout:,
    read_timeout:,
    force_http2: false
  )
  response = client.get(path)
  response.body
  response
ensure
  client&.close
end

.split_url(url) ⇒ [String, String]

Parameters:

  • url (String)

Returns:

  • ([String, String])


452
453
454
455
456
457
458
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 452

def split_url(url)
  uri = URI.parse(url)
  port = uri.port unless uri.default_port == uri.port
  origin = "#{uri.scheme}://#{uri.host}#{":#{port}" if port}"

  [origin, uri.request_uri]
end

Instance Method Details

#closenil

Returns:

  • (nil)


611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 611

def close
  return unless mark_closed

  close_error = nil
  scheduler = Fiber.scheduler
  begin
    evict_client_from_scheduler(scheduler) if scheduler
  rescue StandardError => e
    close_error ||= e
  end

  begin
    close_thread_clients
  rescue StandardError => e
    close_error ||= e
  end

  raise close_error if close_error
end

#get(path) ⇒ Response

Parameters:

  • path (String)

Returns:



585
586
587
588
589
590
591
592
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 585

def get(path)
  ensure_open!
  build_response(stream: false) do |yielder, status_assigner, headers_assigner|
    execute_request(:get, path, [[], nil],
                    stream: false,
                    response_handlers: [yielder, status_assigner, headers_assigner])
  end
end

#post(path, form: nil, json: nil, stream: false) ⇒ Object



575
576
577
578
579
580
581
582
583
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 575

def post(path, form: nil, json: nil, stream: false)
  ensure_open!
  headers, body = request_body(form:, json:)
  build_response(stream:) do |yielder, status_assigner, headers_assigner|
    execute_request(:post, path, [headers, body],
                    stream:,
                    response_handlers: [yielder, status_assigner, headers_assigner])
  end
end

#post_bidi(path, headers:) ⇒ [untyped, Response]

Bidirectional HTTP/2 streaming POST. Returns [output, response] where:

  • output is a Protocol::HTTP::Body::Writable::Output (supports << and close)
  • response is a lazy Response whose body is consumed via Response#each

The caller writes NDJSON lines to output while concurrently reading response chunks. Calling output.close sends END_STREAM on the HTTP/2 stream.

Parameters:

  • path (String)
  • headers: (Array[[String, String]])

Returns:



600
601
602
603
604
605
606
607
608
609
# File 'lib/react_on_rails_pro/renderer_http_client.rb', line 600

def post_bidi(path, headers:)
  ensure_open!
  writable = Protocol::HTTP::Body::Writable.new
  response = build_response(stream: true) do |yielder, status_assigner, headers_assigner|
    execute_request(:post, path, [headers, writable],
                    stream: true,
                    response_handlers: [yielder, status_assigner, headers_assigner])
  end
  [writable.output, response]
end