react_on_rails_pro 16.4.0.rc.8 → 16.4.0.rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/LICENSE_SETUP.md +10 -5
- data/lib/react_on_rails_pro/compression_middleware_guard.rb +164 -0
- data/lib/react_on_rails_pro/engine.rb +8 -54
- data/lib/react_on_rails_pro/version.rb +1 -1
- data/lib/react_on_rails_pro.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c63303b689f4adb5d1be96c2415522aeb05fd210258535c2a8fe3421414870a9
|
|
4
|
+
data.tar.gz: d0e63c69ac3c6ca63021ed54fc6f8f1fa6c574f0a261e74d6d6b4b9970a05c8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 920e935410da89b4bbd5951c70e1b4f528e7525bba8f2d27094fbe1ce0676ac17f1277a100d235d6d74186a3414765e40f471de7dd0b7bb7e4faa68d04c61426
|
|
7
|
+
data.tar.gz: a4d980d3b3bdb5ace4a9d18902dbfabb35c2c2c99a3c354e5cbc39bda05853500d466d34c748e612dea4e0f0886849ed255be92082274befbaabc971af6b327e
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (16.4.0.rc.
|
|
12
|
+
react_on_rails (16.4.0.rc.9)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,7 +20,7 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (16.4.0.rc.
|
|
23
|
+
react_on_rails_pro (16.4.0.rc.9)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.6)
|
|
26
26
|
connection_pool
|
|
@@ -29,7 +29,7 @@ PATH
|
|
|
29
29
|
httpx (~> 1.5)
|
|
30
30
|
jwt (~> 2.7)
|
|
31
31
|
rainbow
|
|
32
|
-
react_on_rails (= 16.4.0.rc.
|
|
32
|
+
react_on_rails (= 16.4.0.rc.9)
|
|
33
33
|
|
|
34
34
|
GEM
|
|
35
35
|
remote: https://rubygems.org/
|
data/LICENSE_SETUP.md
CHANGED
|
@@ -75,13 +75,16 @@ heroku config:set REACT_ON_RAILS_PRO_LICENSE="your_token"
|
|
|
75
75
|
Configure your license token via the `REACT_ON_RAILS_PRO_LICENSE` environment variable.
|
|
76
76
|
Never commit license tokens to version control.
|
|
77
77
|
|
|
78
|
-
## License Validation
|
|
78
|
+
## License Validation and Signals
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
License-related checks and signals occur at multiple points:
|
|
81
81
|
|
|
82
82
|
1. **Ruby Gem**: When Rails application starts
|
|
83
83
|
2. **Node Renderer**: When the Node renderer process starts
|
|
84
|
-
3. **Browser Package**:
|
|
84
|
+
3. **Browser Package**: Receives Pro-installed signal via `railsContext.rorPro` (not license-valid state)
|
|
85
|
+
|
|
86
|
+
The browser package does not perform independent license validation. A valid paid license is still required for
|
|
87
|
+
production deployments.
|
|
85
88
|
|
|
86
89
|
When no license is present, the application runs in **unlicensed mode**. This is fine for development, testing, and CI/CD. Production deployments should always have a valid paid license.
|
|
87
90
|
|
|
@@ -148,7 +151,7 @@ The task exits with code 0 on success and code 1 if the license is missing, inva
|
|
|
148
151
|
| Field | Type | Description |
|
|
149
152
|
| ---------------------- | --------------- | --------------------------------------------------- |
|
|
150
153
|
| `status` | string | `"valid"`, `"expired"`, `"invalid"`, or `"missing"` |
|
|
151
|
-
| `organization` | string or null | Organization name from the
|
|
154
|
+
| `organization` | string or null | Organization name from the JWT `org` claim |
|
|
152
155
|
| `plan` | string or null | License plan (`"paid"`, `"startup"`, etc.) |
|
|
153
156
|
| `expiration` | string or null | ISO 8601 expiration date |
|
|
154
157
|
| `attribution_required` | boolean | Whether attribution is required |
|
|
@@ -302,11 +305,13 @@ The license is a JWT (JSON Web Token) signed with RSA-256, containing:
|
|
|
302
305
|
"iat": 1234567890, // Issued at timestamp (REQUIRED)
|
|
303
306
|
"exp": 1234567890, // Expiration timestamp (REQUIRED)
|
|
304
307
|
"plan": "paid", // License plan (Optional — only "paid" is valid for production)
|
|
305
|
-
"
|
|
308
|
+
"org": "Your Company", // Organization name (Optional)
|
|
306
309
|
"iss": "api" // Issuer identifier (Optional, standard JWT claim)
|
|
307
310
|
}
|
|
308
311
|
```
|
|
309
312
|
|
|
313
|
+
> Note: The JWT claim is `org`. The verify task output uses the field name `organization` for readability.
|
|
314
|
+
|
|
310
315
|
### Security
|
|
311
316
|
|
|
312
317
|
- **Offline validation**: No internet connection required
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module ReactOnRailsPro
|
|
7
|
+
class CompressionMiddlewareGuard
|
|
8
|
+
COMPATIBILITY_GUIDE_PATH =
|
|
9
|
+
"https://www.shakacode.com/react-on-rails/docs/building-features/" \
|
|
10
|
+
"streaming-server-rendering/#compression-middleware-compatibility"
|
|
11
|
+
PROBLEMATIC_MIDDLEWARES = %w[Rack::Deflater Rack::Brotli].freeze
|
|
12
|
+
PROBE_TIMEOUT_SECONDS = 1
|
|
13
|
+
|
|
14
|
+
Finding = Struct.new(:middleware_name, :source_location, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
def initialize(middlewares:, logger: nil)
|
|
17
|
+
@middlewares = normalize_middlewares(middlewares)
|
|
18
|
+
@logger = logger
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def findings
|
|
22
|
+
@findings ||= @middlewares.filter_map do |middleware|
|
|
23
|
+
next unless problematic_middleware?(middleware)
|
|
24
|
+
|
|
25
|
+
condition = middleware_condition(middleware)
|
|
26
|
+
next unless condition.respond_to?(:call)
|
|
27
|
+
next unless destructively_iterates_stream?(condition)
|
|
28
|
+
|
|
29
|
+
Finding.new(
|
|
30
|
+
middleware_name: middleware_name(middleware),
|
|
31
|
+
source_location: source_location_for(condition)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def warning_messages(root:)
|
|
37
|
+
findings.map do |finding|
|
|
38
|
+
"[React on Rails Pro] #{finding.middleware_name} has a custom `:if` callback" \
|
|
39
|
+
"#{formatted_source_location(finding, root: root)} that calls `body.each`. " \
|
|
40
|
+
"This is incompatible with streaming SSR/RSC and can deadlock `ActionController::Live` responses. " \
|
|
41
|
+
"Remove the custom `:if`, or guard it with " \
|
|
42
|
+
"`return true unless body.respond_to?(:to_ary)` before iterating. " \
|
|
43
|
+
"See #{COMPATIBILITY_GUIDE_PATH}."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def normalize_middlewares(middlewares)
|
|
50
|
+
if defined?(ActionDispatch::MiddlewareStack) && middlewares.is_a?(ActionDispatch::MiddlewareStack)
|
|
51
|
+
return middlewares.middlewares
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Array(middlewares)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def problematic_middleware?(middleware)
|
|
58
|
+
PROBLEMATIC_MIDDLEWARES.include?(middleware_name(middleware))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def middleware_name(middleware)
|
|
62
|
+
middleware.klass.respond_to?(:name) ? middleware.klass.name : middleware.klass.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def middleware_condition(middleware)
|
|
66
|
+
Array(middleware.args).filter_map do |arg|
|
|
67
|
+
next unless arg.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
arg[:if] || arg["if"]
|
|
70
|
+
end.first
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def destructively_iterates_stream?(condition)
|
|
74
|
+
probe = StreamingBodyProbe.new
|
|
75
|
+
|
|
76
|
+
Timeout.timeout(PROBE_TIMEOUT_SECONDS) do
|
|
77
|
+
condition.call(probe_env, 200, probe_headers, probe)
|
|
78
|
+
end
|
|
79
|
+
probe.iterated?
|
|
80
|
+
rescue StreamingBodyProbe::BodyIteratedError
|
|
81
|
+
true
|
|
82
|
+
rescue Timeout::Error => e
|
|
83
|
+
return true if probe.iterated?
|
|
84
|
+
|
|
85
|
+
log_probe_failure(condition, e, reason: "timed out after #{PROBE_TIMEOUT_SECONDS}s")
|
|
86
|
+
false
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
return true if probe.iterated?
|
|
89
|
+
|
|
90
|
+
log_probe_failure(condition, e)
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Minimal Rack env used to probe `:if` callbacks.
|
|
95
|
+
# Callbacks that depend on application-specific keys can still raise here;
|
|
96
|
+
# those probe failures are logged at debug level and treated as non-findings.
|
|
97
|
+
# Path-gated callbacks can bypass this probe and yield false negatives.
|
|
98
|
+
def probe_env
|
|
99
|
+
{
|
|
100
|
+
"CONTENT_TYPE" => "text/html; charset=utf-8",
|
|
101
|
+
"HTTP_ACCEPT" => "text/html",
|
|
102
|
+
"REQUEST_METHOD" => "GET",
|
|
103
|
+
"PATH_INFO" => "/__react_on_rails_pro_stream_probe__",
|
|
104
|
+
"HTTP_ACCEPT_ENCODING" => "br, gzip, identity",
|
|
105
|
+
"HTTP_HOST" => "example.test",
|
|
106
|
+
"SERVER_NAME" => "example.test",
|
|
107
|
+
"rack.url_scheme" => "https",
|
|
108
|
+
"rack.errors" => StringIO.new,
|
|
109
|
+
"rack.input" => StringIO.new
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def probe_headers
|
|
114
|
+
{
|
|
115
|
+
"Content-Type" => "text/html; charset=utf-8"
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def formatted_source_location(finding, root:)
|
|
120
|
+
return "" unless finding.source_location
|
|
121
|
+
|
|
122
|
+
path, line = finding.source_location
|
|
123
|
+
root_prefix = "#{root}/"
|
|
124
|
+
display_path = path.start_with?(root_prefix) ? path.delete_prefix(root_prefix) : path
|
|
125
|
+
|
|
126
|
+
" (#{display_path}:#{line})"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def source_location_for(condition)
|
|
130
|
+
condition.respond_to?(:source_location) ? condition.source_location : nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def log_probe_failure(condition, error, reason: nil)
|
|
134
|
+
return unless @logger.respond_to?(:debug)
|
|
135
|
+
|
|
136
|
+
identifier = source_location_for(condition)&.join(":") || condition.class.name || condition.inspect
|
|
137
|
+
backtrace_hint = error.backtrace&.first
|
|
138
|
+
|
|
139
|
+
@logger.debug do
|
|
140
|
+
message = "[React on Rails Pro] CompressionMiddlewareGuard could not probe `:if` callback " \
|
|
141
|
+
"(#{identifier}): "
|
|
142
|
+
message += "#{reason}: " if reason
|
|
143
|
+
message += "#{error.class}: #{error.message}"
|
|
144
|
+
message += " (#{backtrace_hint})" if backtrace_hint
|
|
145
|
+
message
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class StreamingBodyProbe
|
|
150
|
+
include Enumerable
|
|
151
|
+
|
|
152
|
+
class BodyIteratedError < StandardError; end
|
|
153
|
+
|
|
154
|
+
def iterated?
|
|
155
|
+
@iterated == true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def each
|
|
159
|
+
@iterated = true
|
|
160
|
+
raise BodyIteratedError, "Compression middleware `:if` callback called `body.each` on a streaming body."
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -7,10 +7,8 @@ module ReactOnRailsPro
|
|
|
7
7
|
LICENSE_URL = "https://www.shakacode.com/react-on-rails-pro/"
|
|
8
8
|
# TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31).
|
|
9
9
|
LEGACY_LICENSE_FILE = "config/react_on_rails_pro_license.key"
|
|
10
|
-
RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze
|
|
11
10
|
private_constant :LICENSE_URL
|
|
12
11
|
private_constant :LEGACY_LICENSE_FILE
|
|
13
|
-
private_constant :RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS
|
|
14
12
|
|
|
15
13
|
initializer "react_on_rails_pro.routes" do
|
|
16
14
|
ActionDispatch::Routing::Mapper.include ReactOnRailsPro::Routes
|
|
@@ -22,8 +20,8 @@ module ReactOnRailsPro
|
|
|
22
20
|
config.after_initialize { ReactOnRailsPro::Engine.log_license_status }
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
initializer "react_on_rails_pro.
|
|
26
|
-
config.after_initialize { ReactOnRailsPro::Engine.
|
|
23
|
+
initializer "react_on_rails_pro.warn_on_problematic_compression_middleware" do
|
|
24
|
+
config.after_initialize { ReactOnRailsPro::Engine.log_problematic_compression_middleware_warnings }
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
class << self
|
|
@@ -46,22 +44,12 @@ module ReactOnRailsPro
|
|
|
46
44
|
end
|
|
47
45
|
end
|
|
48
46
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return if problematic.empty?
|
|
56
|
-
|
|
57
|
-
route_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
|
|
58
|
-
Rails.logger.warn(
|
|
59
|
-
"[React on Rails Pro] React Server Components support is enabled and the middleware " \
|
|
60
|
-
"stack includes #{problematic.join(', ')}. Compression and other response-transforming " \
|
|
61
|
-
"middleware can interfere with ActionController::Live NDJSON streaming. If your " \
|
|
62
|
-
"`#{route_path}` payload route is not already exempt, consider bypassing " \
|
|
63
|
-
"#{problematic.join(', ')} for that endpoint if you see stalled or corrupted RSC payloads."
|
|
64
|
-
)
|
|
47
|
+
def log_problematic_compression_middleware_warnings(logger: Rails.logger,
|
|
48
|
+
middlewares: Rails.application.middleware,
|
|
49
|
+
root: Rails.root)
|
|
50
|
+
CompressionMiddlewareGuard.new(middlewares: middlewares, logger: logger)
|
|
51
|
+
.warning_messages(root: root)
|
|
52
|
+
.each { |message| logger.warn(message) }
|
|
65
53
|
end
|
|
66
54
|
|
|
67
55
|
private
|
|
@@ -129,40 +117,6 @@ module ReactOnRailsPro
|
|
|
129
117
|
Rails.logger.info message
|
|
130
118
|
end
|
|
131
119
|
end
|
|
132
|
-
|
|
133
|
-
def middleware_stack_names
|
|
134
|
-
middleware_stack = Rails.application&.middleware
|
|
135
|
-
return [] unless middleware_stack
|
|
136
|
-
|
|
137
|
-
entries =
|
|
138
|
-
if middleware_stack.respond_to?(:middlewares)
|
|
139
|
-
middleware_stack.middlewares
|
|
140
|
-
elsif middleware_stack.respond_to?(:to_a)
|
|
141
|
-
middleware_stack.to_a
|
|
142
|
-
else
|
|
143
|
-
Array(middleware_stack)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
entries.filter_map { |entry| middleware_entry_name(entry) }.uniq
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def middleware_entry_name(entry)
|
|
150
|
-
candidate =
|
|
151
|
-
if entry.respond_to?(:klass) && entry.klass
|
|
152
|
-
entry.klass
|
|
153
|
-
elsif entry.is_a?(Array)
|
|
154
|
-
entry.first
|
|
155
|
-
else
|
|
156
|
-
entry
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
case candidate
|
|
160
|
-
when Module
|
|
161
|
-
candidate.name
|
|
162
|
-
else
|
|
163
|
-
candidate.to_s.presence
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
120
|
end
|
|
167
121
|
end
|
|
168
122
|
end
|
data/lib/react_on_rails_pro.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "react_on_rails"
|
|
|
6
6
|
require "react_on_rails_pro/request"
|
|
7
7
|
require "react_on_rails_pro/version"
|
|
8
8
|
require "react_on_rails_pro/constants"
|
|
9
|
+
require "react_on_rails_pro/compression_middleware_guard"
|
|
9
10
|
require "react_on_rails_pro/engine"
|
|
10
11
|
require "react_on_rails_pro/error"
|
|
11
12
|
require "react_on_rails_pro/utils"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react_on_rails_pro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.4.0.rc.
|
|
4
|
+
version: 16.4.0.rc.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -128,14 +128,14 @@ dependencies:
|
|
|
128
128
|
requirements:
|
|
129
129
|
- - '='
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: 16.4.0.rc.
|
|
131
|
+
version: 16.4.0.rc.9
|
|
132
132
|
type: :runtime
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - '='
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: 16.4.0.rc.
|
|
138
|
+
version: 16.4.0.rc.9
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
140
|
name: bundler
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -230,6 +230,7 @@ files:
|
|
|
230
230
|
- lib/react_on_rails_pro/assets_precompile.rb
|
|
231
231
|
- lib/react_on_rails_pro/async_value.rb
|
|
232
232
|
- lib/react_on_rails_pro/cache.rb
|
|
233
|
+
- lib/react_on_rails_pro/compression_middleware_guard.rb
|
|
233
234
|
- lib/react_on_rails_pro/concerns/async_rendering.rb
|
|
234
235
|
- lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb
|
|
235
236
|
- lib/react_on_rails_pro/concerns/stream.rb
|