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.
- checksums.yaml +4 -4
- data/README.md +200 -230
- data/lib/universal_renderer/client/base.rb +75 -0
- data/lib/universal_renderer/client/stream/error_logger.rb +31 -0
- data/lib/universal_renderer/client/stream/execution.rb +94 -0
- data/lib/universal_renderer/client/stream/setup.rb +39 -0
- data/lib/universal_renderer/client/stream.rb +86 -0
- data/lib/universal_renderer/engine.rb +3 -0
- data/{app/controllers/concerns → lib}/universal_renderer/rendering.rb +68 -32
- data/lib/universal_renderer/ssr/helpers.rb +45 -0
- data/lib/universal_renderer/ssr/scrubber.rb +62 -0
- data/lib/universal_renderer/version.rb +1 -1
- data/lib/universal_renderer.rb +5 -1
- metadata +10 -16
- data/app/helpers/universal_renderer/ssr_helpers.rb +0 -19
- data/app/services/universal_renderer/static_client.rb +0 -56
- data/app/services/universal_renderer/stream_client/error_logger.rb +0 -29
- data/app/services/universal_renderer/stream_client/execution.rb +0 -88
- data/app/services/universal_renderer/stream_client/setup.rb +0 -37
- data/app/services/universal_renderer/stream_client.rb +0 -84
- data/lib/universal_renderer/ssr_scrubber.rb +0 -61
@@ -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
|
@@ -4,22 +4,50 @@ module UniversalRenderer
|
|
4
4
|
|
5
5
|
included do
|
6
6
|
include ActionController::Live
|
7
|
-
helper UniversalRenderer::
|
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
|
-
|
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::
|
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
|
-
|
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
|
65
|
-
# If the prop exists but is not an array (e.g., set as a scalar by `
|
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
|
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
|
-
|
121
|
-
|
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
|
data/lib/universal_renderer.rb
CHANGED
@@ -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/
|
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.
|
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-
|
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/
|
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
|