universal_renderer 0.2.4 → 0.3.0

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.
@@ -0,0 +1,39 @@
1
+ module UniversalRenderer
2
+ module Client
3
+ class Stream
4
+ module Setup
5
+ class << self
6
+ def _ensure_ssr_server_url_configured?(config)
7
+ config.ssr_url.present?
8
+ end
9
+
10
+ def _build_stream_request_components(body, config)
11
+ # Ensure ssr_url is present, though _ensure_ssr_server_url_configured? should have caught this.
12
+ # However, direct calls to this method might occur, so a check or reliance on config.ssr_url is important.
13
+ if config.ssr_url.blank?
14
+ raise ArgumentError, "SSR URL is not configured."
15
+ end
16
+
17
+ parsed_ssr_url = URI.parse(config.ssr_url)
18
+ stream_uri = URI.join(parsed_ssr_url, config.ssr_stream_path)
19
+
20
+ http = Net::HTTP.new(stream_uri.host, stream_uri.port)
21
+ http.use_ssl = (stream_uri.scheme == "https")
22
+ http.open_timeout = config.timeout
23
+ http.read_timeout = config.timeout
24
+
25
+ http_request =
26
+ Net::HTTP::Post.new(
27
+ stream_uri.request_uri,
28
+ "Content-Type" => "application/json"
29
+ )
30
+
31
+ http_request.body = body.to_json
32
+
33
+ [stream_uri, http, http_request]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stream/error_logger"
4
+ require_relative "stream/execution"
5
+ require_relative "stream/setup"
6
+
7
+ module UniversalRenderer
8
+ module Client
9
+ class Stream
10
+ extend ErrorLogger
11
+ extend Execution
12
+ extend Setup
13
+
14
+ # Orchestrates the streaming process for server-side rendering.
15
+ #
16
+ # @param url [String] The URL of the page to render.
17
+ # @param props [Hash] Data to be passed for rendering, including layout HTML.
18
+ # @param template [String] The HTML template to use for rendering.
19
+ # @param response [ActionDispatch::Response] The Rails response object to stream to.
20
+ # @return [Boolean] True if streaming was initiated, false otherwise.
21
+ def self.stream(url, props, template, response)
22
+ config = UniversalRenderer.config
23
+
24
+ unless Setup._ensure_ssr_server_url_configured?(config)
25
+ Rails.logger.warn(
26
+ "Stream: SSR URL (config.ssr_url) is not configured. Falling back."
27
+ )
28
+ return false
29
+ end
30
+
31
+ stream_uri_obj = nil
32
+ full_ssr_url_for_log = config.ssr_url.to_s # For logging in case of early error
33
+
34
+ begin
35
+ body = { url: url, props: props, template: template }
36
+
37
+ actual_stream_uri, http_client, http_post_request =
38
+ Setup._build_stream_request_components(body, config)
39
+
40
+ stream_uri_obj = actual_stream_uri
41
+
42
+ full_ssr_url_for_log = actual_stream_uri.to_s # Update for more specific logging
43
+ rescue URI::InvalidURIError => e
44
+ Rails.logger.error(
45
+ "Stream: SSR stream failed due to invalid URI ('#{config.ssr_url}'): #{e.message}"
46
+ )
47
+
48
+ return false
49
+ rescue StandardError => e
50
+ _log_setup_error(e, full_ssr_url_for_log)
51
+
52
+ return false
53
+ end
54
+
55
+ Execution._perform_streaming(
56
+ http_client,
57
+ http_post_request,
58
+ response,
59
+ stream_uri_obj
60
+ )
61
+ rescue Errno::ECONNREFUSED,
62
+ Errno::EHOSTUNREACH,
63
+ Net::OpenTimeout,
64
+ Net::ReadTimeout,
65
+ SocketError => e
66
+ uri_str_for_conn_error =
67
+ stream_uri_obj ? stream_uri_obj.to_s : full_ssr_url_for_log
68
+
69
+ ErrorLogger._log_connection_error(e, uri_str_for_conn_error)
70
+
71
+ false
72
+ rescue StandardError => e
73
+ uri_str_for_unexpected_error =
74
+ stream_uri_obj ? stream_uri_obj.to_s : full_ssr_url_for_log
75
+
76
+ ErrorLogger._log_unexpected_error(
77
+ e,
78
+ uri_str_for_unexpected_error,
79
+ "Stream: Unexpected error during SSR stream process"
80
+ )
81
+
82
+ false
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,4 +1,7 @@
1
1
  module UniversalRenderer
2
2
  class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include UniversalRenderer::Rendering
5
+ end
3
6
  end
4
7
  end
@@ -4,22 +4,50 @@ module UniversalRenderer
4
4
 
5
5
  included do
6
6
  include ActionController::Live
7
- helper UniversalRenderer::SsrHelpers
7
+ helper UniversalRenderer::SSR::Helpers
8
8
  before_action :initialize_props
9
9
  end
10
10
 
11
+ class_methods do
12
+ def enable_ssr(options = {})
13
+ class_attribute :enable_ssr, instance_writer: false
14
+ self.enable_ssr = true
15
+
16
+ class_attribute :ssr_streaming_preference, instance_writer: false
17
+ self.ssr_streaming_preference = options[:streaming]
18
+ end
19
+ end
20
+
21
+ # Fetches Server-Side Rendered (SSR) content for the current request.
22
+ # This method makes a blocking call to the SSR service using {UniversalRenderer::Client::Base.fetch}
23
+ # and stores the result in the `@ssr` instance variable.
24
+ #
25
+ # The SSR content is fetched based on the `request.original_url` and the
26
+ # `@universal_renderer_props` accumulated for the request.
27
+ #
28
+ # @return [Hash, nil] The fetched SSR data (typically a hash with keys like `:head`, `:body_html`, `:body_attrs`),
29
+ # or `nil` if the fetch fails or SSR is not configured.
30
+ def fetch_ssr
31
+ @ssr =
32
+ UniversalRenderer::Client::Base.fetch(
33
+ request.original_url,
34
+ @universal_renderer_props
35
+ )
36
+ end
37
+
38
+ def use_ssr_streaming?
39
+ self.class.try(:ssr_streaming_preference)
40
+ end
41
+
11
42
  private
12
43
 
13
44
  def default_render
14
- return super unless request.format.html?
45
+ return super unless self.class.enable_ssr && request.format.html?
46
+
15
47
  if use_ssr_streaming?
16
48
  render_ssr_stream
17
49
  else
18
- @ssr =
19
- UniversalRenderer::StaticClient.static(
20
- request.original_url,
21
- @universal_renderer_props
22
- )
50
+ fetch_ssr
23
51
  super
24
52
  end
25
53
  end
@@ -38,7 +66,7 @@ module UniversalRenderer
38
66
  current_props = @universal_renderer_props.dup
39
67
 
40
68
  streaming_succeeded =
41
- UniversalRenderer::StreamClient.stream(
69
+ UniversalRenderer::Client::Stream.stream(
42
70
  request.original_url,
43
71
  current_props,
44
72
  after_meta,
@@ -52,7 +80,18 @@ module UniversalRenderer
52
80
  @universal_renderer_props = {}
53
81
  end
54
82
 
55
- def add_props(key_or_hash, data_value = nil)
83
+ # Adds a prop or a hash of props to be sent to the SSR service.
84
+ # Props are deep-stringified if a hash is provided.
85
+ #
86
+ # @param key_or_hash [String, Symbol, Hash] The key for the prop or a hash of props.
87
+ # @param data_value [Object, nil] The value for the prop if `key_or_hash` is a key.
88
+ # If `key_or_hash` is a Hash, this parameter is ignored.
89
+ # @example Adding a single prop
90
+ # add_prop(:user_id, 123)
91
+ # @example Adding multiple props from a hash
92
+ # add_prop({theme: "dark", locale: "en"})
93
+ # @return [void]
94
+ def add_prop(key_or_hash, data_value = nil)
56
95
  if data_value.nil? && key_or_hash.is_a?(Hash)
57
96
  @universal_renderer_props.merge!(key_or_hash.deep_stringify_keys)
58
97
  else
@@ -61,10 +100,22 @@ module UniversalRenderer
61
100
  end
62
101
 
63
102
  # Allows a prop to be treated as an array, pushing new values to it.
64
- # If the prop does not exist or is nil, it's initialized as an array.
65
- # If the prop exists but is not an array (e.g., set as a scalar by `add_props`),
103
+ # If the prop does not exist or is `nil`, it\'s initialized as an empty array.
104
+ # If the prop exists but is not an array (e.g., set as a scalar by `add_prop`),
66
105
  # its current value will be converted into the first element of the new array.
67
- # If `value_to_add` is an array, its elements are concatenated. Otherwise, `value_to_add` is appended.
106
+ # If `value_to_add` is an array, its elements are concatenated to the existing array.
107
+ # Otherwise, `value_to_add` is appended as a single element.
108
+ #
109
+ # @param key [String, Symbol] The key of the prop to modify.
110
+ # @param value_to_add [Object, Array] The value or array of values to add to the prop.
111
+ # @example Pushing a single value
112
+ # push_prop(:notifications, "New message")
113
+ # @example Pushing multiple values from an array
114
+ # push_prop(:tags, ["rails", "ruby"])
115
+ # @example Appending to an existing scalar value (converts to array)
116
+ # add_prop(:item, "first")
117
+ # push_prop(:item, "second") # @universal_renderer_props becomes { "item" => ["first", "second"] }
118
+ # @return [void]
68
119
  def push_prop(key, value_to_add)
69
120
  prop_key = key.to_s
70
121
  current_value = @universal_renderer_props[prop_key]
@@ -83,10 +134,6 @@ module UniversalRenderer
83
134
  end
84
135
  end
85
136
 
86
- def use_ssr_streaming?
87
- %w[1 true yes y].include?(ENV["ENABLE_SSR_STREAMING"]&.downcase)
88
- end
89
-
90
137
  def set_streaming_headers
91
138
  # Tell Cloudflare / proxies not to cache or buffer.
92
139
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
@@ -103,30 +150,19 @@ module UniversalRenderer
103
150
 
104
151
  def handle_ssr_stream_fallback(response)
105
152
  # SSR streaming failed or was not possible (e.g. server down, config missing).
106
- # Ensure response hasn't been touched in a way that prevents a new render.
153
+ # Ensure response hasn\'t been touched in a way that prevents a new render.
107
154
  return unless response.committed? || response.body.present?
108
155
 
109
156
  Rails.logger.error(
110
157
  "SSR stream fallback:" \
111
158
  "Cannot render default fallback template because response was already committed or body present."
112
159
  )
113
- # Close the stream if it's still open to prevent client connection from hanging
114
- # when we can't render a fallback page due to already committed response
160
+ # Close the stream if it\'s still open to prevent client connection from hanging
161
+ # when we can\'t render a fallback page due to already committed response
115
162
  response.stream.close unless response.stream.closed?
116
- # If response not committed, no explicit render is called here,
117
- # allowing Rails' default rendering behavior to take over.
118
- end
119
163
 
120
- # Overrides the built-in render_to_string.
121
- # If you call render_to_string with no explicit template/partial/inline,
122
- # it will fall back to 'ssr/index'.
123
- def render_to_string(options = {}, *args, &block)
124
- if options.is_a?(Hash) && !options.key?(:template) &&
125
- !options.key?(:partial) && !options.key?(:inline) &&
126
- !options.key?(:json) && !options.key?(:xml)
127
- options = options.merge(template: "application/index")
128
- end
129
- super(options, *args, &block)
164
+ # If response not committed, no explicit render is called here,
165
+ # allowing Rails\' default rendering behavior to take over.
130
166
  end
131
167
  end
132
168
  end
@@ -0,0 +1,45 @@
1
+ module UniversalRenderer
2
+ module SSR
3
+ module Helpers
4
+ # @!method ssr_meta
5
+ # Outputs a meta tag placeholder for SSR content.
6
+ # This placeholder is used by the rendering process to inject SSR metadata.
7
+ # @return [String] The HTML-safe string "<!-- SSR_META -->".
8
+ def ssr_meta
9
+ "<!-- SSR_META -->".html_safe
10
+ end
11
+
12
+ # @!method ssr_body
13
+ # Outputs a body placeholder for SSR content.
14
+ # This placeholder is used by the rendering process to inject the main SSR body.
15
+ # @return [String] The HTML-safe string "<!-- SSR_BODY -->".
16
+ def ssr_body
17
+ "<!-- SSR_BODY -->".html_safe
18
+ end
19
+
20
+ # @!method sanitize_ssr(html)
21
+ # Sanitizes HTML content rendered by the SSR service.
22
+ # Uses a custom scrubber ({UniversalRenderer::SSR::Scrubber}) to remove potentially
23
+ # harmful elements like scripts and event handlers, while allowing safe tags
24
+ # like stylesheets and meta tags.
25
+ # @param html [String] The HTML string to sanitize.
26
+ # @return [String] The sanitized HTML string.
27
+ def sanitize_ssr(html)
28
+ sanitize(html, scrubber: Scrubber.new)
29
+ end
30
+
31
+ # @!method use_ssr_streaming?
32
+ # Determines if SSR streaming should be used for the current request.
33
+ # The decision is based solely on the `ssr_streaming_preference` class attribute
34
+ # set on the controller.
35
+ # - If `ssr_streaming_preference` is `true`, streaming is enabled.
36
+ # - If `ssr_streaming_preference` is `false`, streaming is disabled.
37
+ # - If `ssr_streaming_preference` is `nil` (not set), streaming is disabled.
38
+ # @return [Boolean, nil] The value of `ssr_streaming_preference` (true, false, or nil).
39
+ # In conditional contexts, `nil` will behave as `false`.
40
+ def use_ssr_streaming?
41
+ controller.use_ssr_streaming?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,62 @@
1
+ require "loofah"
2
+
3
+ module UniversalRenderer
4
+ module SSR
5
+ class Scrubber < ::Loofah::Scrubber
6
+ def initialize
7
+ super
8
+ @direction = :top_down
9
+ end
10
+
11
+ def scrub(node)
12
+ # Primary actions: stop if script, continue if a passthrough tag, otherwise clean attributes.
13
+ return Loofah::Scrubber::STOP if handle_script_node(node) # Checks for <script> and removes it
14
+
15
+ # Allows <link rel="stylesheet">, <style>, <meta> to pass through this scrubber.
16
+ return Loofah::Scrubber::CONTINUE if passthrough_node?(node)
17
+
18
+ # For all other nodes, clean potentially harmful attributes.
19
+ clean_attributes(node)
20
+ # Default Loofah behavior (CONTINUE for children) applies if not returned earlier.
21
+ end
22
+
23
+ private
24
+
25
+ # Handles <script> tags: removes them and returns true if a script node was processed.
26
+ def handle_script_node(node)
27
+ return false unless node.name == "script"
28
+
29
+ node.remove
30
+ true # Indicates the node was a script and has been handled.
31
+ end
32
+
33
+ # Checks if the node is a type that should bypass detailed attribute scrubbing.
34
+ def passthrough_node?(node)
35
+ (node.name == "link" && node["rel"]&.to_s&.downcase == "stylesheet") ||
36
+ %w[style meta].include?(node.name)
37
+ end
38
+
39
+ # Orchestrates the cleaning of attributes for a given node.
40
+ def clean_attributes(node)
41
+ remove_javascript_href(node)
42
+ remove_event_handlers(node)
43
+ end
44
+
45
+ # Removes "javascript:" hrefs from <a> tags.
46
+ def remove_javascript_href(node)
47
+ return unless node.name == "a" && node["href"]
48
+ href = node["href"].to_s.downcase
49
+ node.remove_attribute("href") if href.start_with?("javascript:")
50
+ end
51
+
52
+ # Removes "on*" event handler attributes from any node.
53
+ def remove_event_handlers(node)
54
+ attrs_to_remove =
55
+ node.attributes.keys.select do |name|
56
+ name.to_s.downcase.start_with?("on")
57
+ end
58
+ attrs_to_remove.each { |attr_name| node.remove_attribute(attr_name) }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,3 @@
1
1
  module UniversalRenderer
2
- VERSION = "0.2.4".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
@@ -1,7 +1,11 @@
1
1
  require "universal_renderer/version"
2
2
  require "universal_renderer/engine"
3
3
  require "universal_renderer/configuration"
4
- require "universal_renderer/ssr_scrubber"
4
+ require "universal_renderer/ssr/scrubber"
5
+ require "universal_renderer/client/base"
6
+ require "universal_renderer/client/stream"
7
+ require "universal_renderer/rendering"
8
+ require "universal_renderer/ssr/helpers"
5
9
 
6
10
  module UniversalRenderer
7
11
  class << self
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: universal_renderer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thaske
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-20 00:00:00.000000000 Z
10
+ date: 2025-05-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: loofah
@@ -30,9 +30,6 @@ dependencies:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '7.1'
33
- - - ">="
34
- - !ruby/object:Gem::Version
35
- version: 7.1.5.1
36
33
  type: :runtime
37
34
  prerelease: false
38
35
  version_requirements: !ruby/object:Gem::Requirement
@@ -40,9 +37,6 @@ dependencies:
40
37
  - - "~>"
41
38
  - !ruby/object:Gem::Version
42
39
  version: '7.1'
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: 7.1.5.1
46
40
  description: Provides helper methods and configuration to forward rendering requests
47
41
  from a Rails app to an external SSR server and return the response.
48
42
  email:
@@ -54,21 +48,21 @@ files:
54
48
  - MIT-LICENSE
55
49
  - README.md
56
50
  - Rakefile
57
- - app/controllers/concerns/universal_renderer/rendering.rb
58
- - app/helpers/universal_renderer/ssr_helpers.rb
59
- - app/services/universal_renderer/static_client.rb
60
- - app/services/universal_renderer/stream_client.rb
61
- - app/services/universal_renderer/stream_client/error_logger.rb
62
- - app/services/universal_renderer/stream_client/execution.rb
63
- - app/services/universal_renderer/stream_client/setup.rb
64
51
  - config/routes.rb
65
52
  - lib/generators/universal_renderer/install_generator.rb
66
53
  - lib/generators/universal_renderer/templates/initializer.rb
67
54
  - lib/tasks/universal_renderer_tasks.rake
68
55
  - lib/universal_renderer.rb
56
+ - lib/universal_renderer/client/base.rb
57
+ - lib/universal_renderer/client/stream.rb
58
+ - lib/universal_renderer/client/stream/error_logger.rb
59
+ - lib/universal_renderer/client/stream/execution.rb
60
+ - lib/universal_renderer/client/stream/setup.rb
69
61
  - lib/universal_renderer/configuration.rb
70
62
  - lib/universal_renderer/engine.rb
71
- - lib/universal_renderer/ssr_scrubber.rb
63
+ - lib/universal_renderer/rendering.rb
64
+ - lib/universal_renderer/ssr/helpers.rb
65
+ - lib/universal_renderer/ssr/scrubber.rb
72
66
  - lib/universal_renderer/version.rb
73
67
  homepage: https://github.com/thaske/universal_renderer
74
68
  licenses:
@@ -1,19 +0,0 @@
1
- module UniversalRenderer
2
- module SsrHelpers
3
- def ssr_meta
4
- "<!-- SSR_META -->".html_safe
5
- end
6
-
7
- def ssr_body
8
- "<!-- SSR_BODY -->".html_safe
9
- end
10
-
11
- def sanitize_ssr(html)
12
- sanitize(html, scrubber: SsrScrubber.new)
13
- end
14
-
15
- def use_ssr_streaming?
16
- %w[1 true yes y].include?(ENV["ENABLE_SSR_STREAMING"]&.downcase)
17
- end
18
- end
19
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "json" # Ensure JSON is required for parsing and generation
5
- require "uri" # Ensure URI is required for URI parsing
6
-
7
- module UniversalRenderer
8
- class StaticClient
9
- class << self
10
- # Performs a POST request to the SSR service to get statically rendered content.
11
- #
12
- # @param url [String] The URL of the page to render.
13
- # @param props [Hash] Additional data to be passed for rendering (renamed from query_data for clarity).
14
- # @return [Hash, nil] The parsed JSON response as symbolized keys if successful, otherwise nil.
15
- def static(url, props)
16
- ssr_url = UniversalRenderer.config.ssr_url
17
- return if ssr_url.blank?
18
-
19
- timeout = UniversalRenderer.config.timeout
20
-
21
- begin
22
- uri = URI.parse(ssr_url)
23
- http = Net::HTTP.new(uri.host, uri.port)
24
- http.use_ssl = (uri.scheme == "https")
25
- http.open_timeout = timeout
26
- http.read_timeout = timeout
27
-
28
- request = Net::HTTP::Post.new(uri.request_uri) # Use uri.request_uri to include path if present in ssr_url
29
- request.body = { url: url, props: props }.to_json
30
- request["Content-Type"] = "application/json"
31
-
32
- response = http.request(request)
33
-
34
- if response.is_a?(Net::HTTPSuccess)
35
- JSON.parse(response.body).deep_symbolize_keys
36
- else
37
- Rails.logger.error(
38
- "SSR static request to #{ssr_url} failed: #{response.code} - #{response.message} (URL: #{url})"
39
- )
40
- nil
41
- end
42
- rescue Net::OpenTimeout, Net::ReadTimeout => e
43
- Rails.logger.error(
44
- "SSR static request to #{ssr_url} timed out: #{e.class.name} - #{e.message} (URL: #{url})"
45
- )
46
- nil
47
- rescue StandardError => e
48
- Rails.logger.error(
49
- "SSR static request to #{ssr_url} failed: #{e.class.name} - #{e.message} (URL: #{url})"
50
- )
51
- nil
52
- end
53
- end
54
- end
55
- end
56
- end
@@ -1,29 +0,0 @@
1
- module UniversalRenderer
2
- class StreamClient
3
- module ErrorLogger
4
- class << self
5
- def _log_setup_error(error, target_uri_string)
6
- backtrace_info = error.backtrace&.first || "No backtrace available"
7
- Rails.logger.error(
8
- "Unexpected error during SSR stream setup for #{target_uri_string}: " \
9
- "#{error.class.name} - #{error.message} at #{backtrace_info}"
10
- )
11
- end
12
-
13
- def _log_connection_error(error, target_uri_string)
14
- Rails.logger.error(
15
- "SSR stream connection to #{target_uri_string} failed: #{error.class.name} - #{error.message}"
16
- )
17
- end
18
-
19
- def _log_unexpected_error(error, target_uri_string, context_message)
20
- backtrace_info = error.backtrace&.first || "No backtrace available"
21
- Rails.logger.error(
22
- "#{context_message} for #{target_uri_string}: " \
23
- "#{error.class.name} - #{error.message} at #{backtrace_info}"
24
- )
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,88 +0,0 @@
1
- module UniversalRenderer
2
- class StreamClient
3
- module Execution
4
- class << self
5
- def _perform_streaming(
6
- http_client,
7
- http_post_request,
8
- response,
9
- stream_uri
10
- )
11
- http_client.request(http_post_request) do |node_res|
12
- Execution._handle_node_response_streaming(
13
- node_res,
14
- response,
15
- stream_uri
16
- )
17
-
18
- return true
19
- end
20
- false
21
- end
22
-
23
- def _handle_node_response_streaming(node_res, response, stream_uri)
24
- if node_res.is_a?(Net::HTTPSuccess)
25
- Execution._stream_response_body(node_res, response.stream)
26
- else
27
- Execution._handle_streaming_for_node_error(
28
- node_res,
29
- response,
30
- stream_uri
31
- )
32
- end
33
- rescue StandardError => e
34
- Rails.logger.error(
35
- "Error during SSR data transfer or stream writing from #{stream_uri}: #{e.class.name} - #{e.message}"
36
- )
37
-
38
- Execution._write_generic_html_error(
39
- response.stream,
40
- "Streaming Error",
41
- "<p>A problem occurred while loading content. Please refresh.</p>"
42
- )
43
- ensure
44
- response.stream.close unless response.stream.closed?
45
- end
46
-
47
- def _stream_response_body(source_http_response, target_io_stream)
48
- source_http_response.read_body do |chunk|
49
- target_io_stream.write(chunk)
50
- end
51
- end
52
-
53
- def _handle_streaming_for_node_error(node_res, response, stream_uri)
54
- Rails.logger.error(
55
- "SSR stream server at #{stream_uri} responded with #{node_res.code} #{node_res.message}."
56
- )
57
-
58
- is_potentially_viewable_error =
59
- node_res["Content-Type"]&.match?(%r{text/html}i)
60
-
61
- if is_potentially_viewable_error
62
- Rails.logger.info(
63
- "Attempting to stream HTML error page from Node SSR server."
64
- )
65
- Execution._stream_response_body(node_res, response.stream)
66
- else
67
- Rails.logger.warn(
68
- "Node SSR server error response Content-Type ('#{node_res["Content-Type"]}') is not text/html. " \
69
- "Injecting generic error message into the stream."
70
- )
71
-
72
- Execution._write_generic_html_error(
73
- response.stream,
74
- "Application Error",
75
- "<p>There was an issue rendering this page. Please try again later.</p>"
76
- )
77
- end
78
- end
79
-
80
- def _write_generic_html_error(stream, title_text, message_html_fragment)
81
- return if stream.closed?
82
-
83
- stream.write("<h1>#{title_text}</h1>#{message_html_fragment}")
84
- end
85
- end
86
- end
87
- end
88
- end