Class: ReactOnRailsPro::RollingDeployAdapters::Http

Inherits:
Object
  • Object
show all
Defined in:
lib/react_on_rails_pro/rolling_deploy_adapters/http.rb

Overview

Built-in HTTP rolling-deploy adapter. Pairs with ReactOnRailsPro::RollingDeploy::BundlesController on the running Rails server: the controller exposes the current deployment’s bundles and the adapter (running in the next deployment’s build CI) fetches them.

The promise is “zero-infra default”: no S3 bucket, no IAM, no extra gem. The currently-deployed Rails server already has the bundles + companion assets sitting on disk; this adapter pulls them via authenticated HTTP.

Configuration (see docs/pro/rolling-deploy-adapters.md):

ReactOnRailsPro.configure do |config|
  config.rolling_deploy_adapter      = ReactOnRailsPro::RollingDeployAdapters::Http
  config.rolling_deploy_token        = ENV.fetch("ROLLING_DEPLOY_TOKEN")
  config.rolling_deploy_previous_url = ENV["ROLLING_DEPLOY_PREVIOUS_URL"]
end

Error contract matches the rolling_deploy_adapter protocol: every exception is caught and reported as a warning so a failed seed degrades to the runtime 410-retry fallback rather than failing the build. rubocop:disable Metrics/ClassLength

Constant Summary collapse

DEFAULT_OPEN_TIMEOUT_SECONDS =

Per-request HTTP timeouts. The outer Timeout.timeout in RollingDeployCacheStager bounds the total wall-clock budget (10s for discovery, 30s for fetch); these inner timeouts let a hung server fail before the outer wrapper interrupts mid-write, which is more reliable than relying on the thread-level Timeout.timeout that may interrupt at a random execution point.

5
DEFAULT_READ_TIMEOUT_SECONDS =
25
MANIFEST_READ_TIMEOUT_SECONDS =

Manifest discovery is wrapped in a 10s outer budget by RollingDeployCacheStager.

4
DEFAULT_MAX_SIZE =

Maximum uncompressed payload accepted from /bundles/:hash. Mirrors the tarball helper default so a misbehaving or malicious server cannot exhaust disk via a zip-bomb-style response.

ReactOnRailsPro::RollingDeploy::Tarball::DEFAULT_MAX_SIZE
COMPRESSED_BODY_CAP =

Maximum compressed bytes accepted from /bundles/:hash before extract enforces DEFAULT_MAX_SIZE on the uncompressed tarball contents. Set near 1/4 of DEFAULT_MAX_SIZE: JS bundles typically decompress 3-5x, so a 50 MB wire payload that decompresses beyond 200 MB is anomalous.

50 * 1024 * 1024
LOG_PREFIX =
"[ReactOnRailsPro::RollingDeployAdapters::Http]"
BUNDLE_ENTRY_NAME =

Wire-format constant: must stay in sync with ‘ReactOnRailsPro::RollingDeploy::BundlesController::BUNDLE_ENTRY_NAME`. The controller serves the bundle file under this entry name; if the two ever diverge the client will fail to locate the bundle after extracting the tarball.

"bundle.js"

Class Method Summary collapse

Class Method Details

.fetch(bundle_hash) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/react_on_rails_pro/rolling_deploy_adapters/http.rb', line 100

def fetch(bundle_hash)
  base = configured_previous_url
  return nil if base.nil?
  return nil if hash_invalid?(bundle_hash)

  if token_missing?
    return warn_and_return("rolling_deploy_token is not configured; skipping fetch(#{bundle_hash.inspect})",
                           nil)
  end

  dir = bundle_dir(bundle_hash)
  FileUtils.mkdir_p(dir)

  result = download_bundle_tarball(base, bundle_hash) do |tarball|
    extract_payload(tarball, dir, bundle_hash)
  end
  return cleanup_and_return(dir, nil) if result.nil?

  result
rescue StandardError => e
  cleanup_and_return(dir, nil) if dir
  warn_and_return("fetch(#{bundle_hash.inspect}) failed: #{e.class}: #{e.message}", nil)
end

.previous_bundle_hashesObject



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
# File 'lib/react_on_rails_pro/rolling_deploy_adapters/http.rb', line 70

def previous_bundle_hashes
  base = configured_previous_url
  return [] if base.nil?

  if token_missing?
    return warn_and_return("rolling_deploy_token is not configured; skipping manifest fetch",
                           [])
  end

  response = http_get(
    URI("#{base}/manifest"),
    read_timeout: MANIFEST_READ_TIMEOUT_SECONDS
  )
  return warn_and_return("manifest returned HTTP #{response.code}", []) unless response.is_a?(Net::HTTPSuccess)

  parsed = JSON.parse(response.body)
  # Filter manifest hashes through SAFE_HASH_PATTERN before returning
  # so server-supplied strings never appear verbatim in downstream
  # warning logs. Each hash is re-validated inside `fetch`, so this is
  # defense-in-depth — nothing unsafe could reach the filesystem layer
  # — but it keeps log lines from a misbehaving or compromised server
  # from echoing arbitrary content.
  Array(parsed["hashes"])
    .map(&:to_s)
    .reject(&:empty?)
    .grep(ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN)
rescue StandardError => e
  warn_and_return("previous_bundle_hashes failed: #{e.class}: #{e.message}", [])
end

.upload(_bundle_hash, bundle:, assets:) ⇒ Object

Intentional no-op. The running Rails server IS the artifact store —bundle + companion assets are already on local disk where the mountable BundlesController will serve them on the next deploy’s build CI. Documented in docs/pro/rolling-deploy-adapters.md.



128
129
130
# File 'lib/react_on_rails_pro/rolling_deploy_adapters/http.rb', line 128

def upload(_bundle_hash, bundle:, assets:)
  # See class doc above.
end