Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.4.6"

gem "react_on_rails_pro", "16.6.0"
gem "react_on_rails_pro", "16.7.0.rc.2"
gem "shakapacker", "10.0.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails"
Expand Down
18 changes: 9 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
console (1.34.3)
console (1.35.1)
fiber-annotation
fiber-local (~> 1.1)
json
Expand Down Expand Up @@ -168,13 +168,13 @@ GEM
globalid (1.3.0)
activesupport (>= 6.1)
http-2 (1.1.3)
httpx (1.7.6)
httpx (1.7.8)
http-2 (>= 1.1.3)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
interception (0.5)
io-console (0.8.2)
io-event (1.15.1)
io-event (1.16.0)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
Expand All @@ -184,7 +184,7 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.19.1)
jwt (2.10.2)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
launchy (3.0.1)
Expand Down Expand Up @@ -320,23 +320,23 @@ GEM
erb
psych (>= 4.0.0)
tsort
react_on_rails (16.6.0)
react_on_rails (16.7.0.rc.2)
addressable
connection_pool
execjs (~> 2.5)
rails (>= 5.2)
rainbow (~> 3.0)
shakapacker (>= 6.0)
react_on_rails_pro (16.6.0)
react_on_rails_pro (16.7.0.rc.2)
addressable
async (>= 2.29)
connection_pool
execjs (~> 2.9)
http-2 (>= 1.1.1)
httpx (~> 1.5)
jwt (~> 2.7)
jwt (>= 2.7)
rainbow
react_on_rails (= 16.6.0)
react_on_rails (= 16.7.0.rc.2)
redcarpet (3.6.0)
redis (5.3.0)
redis-client (>= 0.22.0)
Expand Down Expand Up @@ -523,7 +523,7 @@ DEPENDENCIES
rails-html-sanitizer
rails_best_practices
rainbow
react_on_rails_pro (= 16.6.0)
react_on_rails_pro (= 16.7.0.rc.2)
redcarpet
redis (~> 5.0)
rspec-rails (~> 6.0.0)
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
"react-dom": "~19.0.4",
"react-error-boundary": "^4.1.2",
"react-intl": "^6.4.4",
"react-on-rails-pro": "16.6.0",
"react-on-rails-pro-node-renderer": "16.6.0",
"react-on-rails-pro": "16.7.0-rc.2",
"react-on-rails-pro-node-renderer": "16.7.0-rc.2",
"react-on-rails-rsc": "19.0.4",
"react-redux": "^8.1.0",
"react-router": "^6.13.0",
Expand Down
44 changes: 40 additions & 4 deletions spec/requests/server_components_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,48 @@

describe "RSC payload endpoint" do
def parsed_chunks
response.body.each_line.filter_map do |line|
stripped = line.strip
next if stripped.empty?
body = response.body.b
chunks = []

JSON.parse(stripped)
until body.empty?
body = discard_blank_frame_lines(body)
break if body.empty?

chunk, body = parse_length_prefixed_chunk(body)
chunks << chunk
end

chunks
end

def discard_blank_frame_lines(body)
body = body.byteslice(1..) || "".b while body.start_with?("\n")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The while modifier with an assignment on the same line is valid Ruby but uncommon enough to cause a double-take. An explicit loop block is easier to scan:

Suggested change
body = body.byteslice(1..) || "".b while body.start_with?("\n")
def discard_blank_frame_lines(body)
while body.start_with?("\n")
body = body.byteslice(1..) || "".b
end
body
end

body
end

def parse_length_prefixed_chunk(body)
header_end = body.index("\n")
raise "Malformed length-prefixed RSC payload: missing header newline" unless header_end

metadata_json, content_length_hex = body.byteslice(0, header_end).split("\t", 2)
raise "Malformed length-prefixed RSC payload: missing tab separator" unless content_length_hex

content_start = header_end + 1
content_length = Integer(content_length_hex, 16)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer(content_length_hex, 16) raises ArgumentError on invalid hex input, which bypasses the explicit raise "Malformed …" pattern used on every other error path. Consider wrapping it for a consistent failure message:

Suggested change
content_length = Integer(content_length_hex, 16)
content_length = Integer(content_length_hex.strip, 16)

Or wrap with rescue:

      content_length = Integer(content_length_hex, 16)
rescue ArgumentError
  raise "Malformed length-prefixed RSC payload: invalid hex length '#{content_length_hex}'"

content = body.byteslice(content_start, content_length)
raise "Malformed length-prefixed RSC payload: truncated content" unless content&.bytesize == content_length

[
parse_payload_metadata(metadata_json, content),
body.byteslice(content_start + content_length, body.bytesize) || "".b
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

byteslice(offset, length) takes a length as its second argument, not an end index. Passing body.bytesize works (it's large enough to capture everything remaining) but reads as if it were an end index. The endless-range form is both correct and self-documenting:

Suggested change
body.byteslice(content_start + content_length, body.bytesize) || "".b
body.byteslice(content_start + content_length..) || "".b

]
end

def parse_payload_metadata(metadata_json, content)
metadata = JSON.parse(metadata_json.force_encoding(Encoding::UTF_8))
content.force_encoding(Encoding::UTF_8)
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata.delete("payloadType") call is doing double duty — removing the key and returning its value for the comparison. This is idiomatic Ruby but non-obvious at first read. A brief comment clarifies the intent:

Suggested change
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content
# Remove payloadType from the hash and use its value to decide how to decode content.
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content

metadata
end

def expect_valid_rsc_payload
Expand Down
30 changes: 15 additions & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5197,7 +5197,7 @@ fastify-plugin@^5.0.0:
resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz#7083e039d6418415f9a669f8c25e72fc5bf2d3e7"
integrity sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==

fastify@^5.8.3:
fastify@^5.8.5:
version "5.8.5"
resolved "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz#c452224295e0ca550bcd0efc3f7d3e90e9c11955"
integrity sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==
Expand Down Expand Up @@ -8783,25 +8783,25 @@ react-is@^18.0.0, react-is@^18.3.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==

react-on-rails-pro-node-renderer@16.6.0:
version "16.6.0"
resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.6.0.tgz#c13ca0f156566531d7c6e005759459b0f19472a8"
integrity sha512-fBZ0lKRaEe8LyVTdUsXx364zQfL6hGJuE+2qQsKo+bXm0aTVq2RtO49gzq0m7Y4xuhBTVmnPQUP0O1v1cGRzLg==
react-on-rails-pro-node-renderer@16.7.0-rc.2:
version "16.7.0-rc.2"
resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.7.0-rc.2.tgz#dbc74bb03b501664835a70533c3db1683ff0549f"
integrity sha512-aMmo/lYbmO8aOQcTF9mJijFiZWU98wx3AN8vMvS/WYjXqCoQ3dgnNb+uLO6XKupjtxQ8W1ZuM31Vs3AC+J6tRQ==
dependencies:
"@fastify/formbody" "^7.4.0 || ^8.0.2"
"@fastify/multipart" "^8.3.1 || ^9.0.3"
fastify "^5.8.3"
fastify "^5.8.5"
fs-extra "^11.2.0"
jsonwebtoken "^9.0.3"
lockfile "^1.0.4"
pino "^9.0.0"

react-on-rails-pro@16.6.0:
version "16.6.0"
resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.6.0.tgz#19a5ea99d7b397dd56f14cff1f31955211b4d0a2"
integrity sha512-Uc8o3gdHyIETvY5J9wVUyONKOhnkw9kGJDREMHQb/IuXoB5/Vo51UK487Rcep2Z+Dzz/bEvNoF+GuZohORZ7Zw==
react-on-rails-pro@16.7.0-rc.2:
version "16.7.0-rc.2"
resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.7.0-rc.2.tgz#105f6f3e7888c4317df6a781fc18a9dbce69b0bb"
integrity sha512-O+Aja01bxtdE5MrYOIbVPl53cJSpC2mo4tz9Ixo1Gx4tyLGCy4C8r01E1126/xHWQl/JFcDBkJlGGjKI/0KnYg==
dependencies:
react-on-rails "16.6.0"
react-on-rails "16.7.0-rc.2"

react-on-rails-rsc@19.0.4:
version "19.0.4"
Expand All @@ -8812,10 +8812,10 @@ react-on-rails-rsc@19.0.4:
neo-async "^2.6.1"
webpack-sources "^3.2.0"

react-on-rails@16.6.0:
version "16.6.0"
resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.6.0.tgz#da7f117fec14f420f7f6ffe6bdb34b7fc2e01b3a"
integrity sha512-LqLi7A0n0Tv5c3yMYlwS9s6rE82gvXNMj3sscmK2LOgIJ+mLQlOX65n0Jq5ZJ4Nsl9SRgxEOOQmcrfvDeB9F1g==
react-on-rails@16.7.0-rc.2:
version "16.7.0-rc.2"
resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.7.0-rc.2.tgz#720553f89f7c4dec2ed26d5113f282195804a99a"
integrity sha512-5tKD5tGBO5K3f5nK7Enp96JPQUCPmKARrm0OeIchniP2r9l7Ax1g0fkbUsGvMLQbzmjTOGVljrdkRPrzQhPK9Q==

react-proxy@^1.1.7:
version "1.1.8"
Expand Down
Loading