Skip to content

Commit 914bb57

Browse files
committed
Don't escape JSON when unnecessary
When `render json:` is used in a controller, and there's no callback option (used for JSONP), the resulting JSON document doesn't need to be HTML-safe (no need to escape HTML entities) or embeddable into JavaScript (no need to escape U+2028 and U+2029). This both saves a costly operation and renders cleaner JSON.
1 parent ab6cb5a commit 914bb57

File tree

3 files changed

+35
-4
lines changed

3 files changed

+35
-4
lines changed

actionpack/lib/action_controller/metal/renderers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def _render_to_body_with_renderer(options)
153153

154154
add :json do |json, options|
155155
json_options = options.except(:callback, :content_type, :status)
156+
json_options[:html_safe] ||= false unless options[:callback].present?
156157
json = json.to_json(json_options) unless json.kind_of?(String)
157158

158159
if options[:callback].present?

actionpack/test/controller/render_json_test.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def render_json_hello_world_with_callback
4646
render json: ActiveSupport::JSON.encode(hello: "world"), callback: "alert"
4747
end
4848

49+
def render_json_unsafe_chars_with_callback
50+
render json: { hello: "\u2028\u2029<script>" }, callback: "alert"
51+
end
52+
53+
def render_json_unsafe_chars_without_callback
54+
render json: { hello: "\u2028\u2029<script>" }
55+
end
56+
4957
def render_json_with_custom_content_type
5058
render json: ActiveSupport::JSON.encode(hello: "world"), content_type: "text/javascript"
5159
end
@@ -106,6 +114,18 @@ def test_render_json_with_callback
106114
assert_equal "text/javascript", @response.media_type
107115
end
108116

117+
def test_render_json_with_callback_escapes_js_chars
118+
get :render_json_unsafe_chars_with_callback, xhr: true
119+
assert_equal '/**/alert({"hello":"\\u2028\\u2029\\u003cscript\\u003e"})', @response.body
120+
assert_equal "text/javascript", @response.media_type
121+
end
122+
123+
def test_render_json_without_callback_does_not_escape_js_chars
124+
get :render_json_unsafe_chars_without_callback
125+
assert_equal %({"hello":"\u2028\u2029<script>"}), @response.body
126+
assert_equal "application/json", @response.media_type
127+
end
128+
109129
def test_render_json_with_custom_content_type
110130
get :render_json_with_custom_content_type, xhr: true
111131
assert_equal '{"hello":"world"}', @response.body
@@ -137,6 +157,6 @@ def test_render_json_calls_to_json_from_object
137157

138158
def test_render_json_avoids_view_options
139159
get :render_json_inspect_options
140-
assert_equal '{"options":{}}', @response.body
160+
assert_equal '{"options":{"html_safe":false}}', @response.body
141161
end
142162
end

activesupport/lib/active_support/json/encoding.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ class << self
2020
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
2121
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
2222
#
23-
# Generates JSON that is safe to include in JavaScript as it escapes
24-
# U+2028 (Line Separator) and U+2029 (Paragraph Separator):
23+
# By default, it generates JSON that is safe to include in JavaScript, as
24+
# it escapes U+2028 (Line Separator) and U+2029 (Paragraph Separator):
2525
#
2626
# ActiveSupport::JSON.encode({ key: "\u2028" })
2727
# # => "{\"key\":\"\\u2028\"}"
@@ -32,11 +32,17 @@ class << self
3232
# ActiveSupport::JSON.encode({ key: "<>&" })
3333
# # => "{\"key\":\"\\u003c\\u003e\\u0026\"}"
3434
#
35-
# This can be changed with the +escape_html_entities+ option, or the
35+
# This behavior can be changed with the +escape_html_entities+ option, or the
3636
# global escape_html_entities_in_json configuration option.
3737
#
3838
# ActiveSupport::JSON.encode({ key: "<>&" }, escape_html_entities: false)
3939
# # => "{\"key\":\"<>&\"}"
40+
#
41+
# For performance reasons, you can set the +html_safe+ option to false,
42+
# which will skip all escaping:
43+
#
44+
# ActiveSupport::JSON.encode({ key: "\u2028<>&" }, html_safe: false)
45+
# # => "{\"key\":\"\u2028<>&\"}"
4046
def encode(value, options = nil)
4147
if options.nil?
4248
Encoding.encode_without_options(value)
@@ -76,6 +82,8 @@ def encode(value)
7682
end
7783
json = stringify(jsonify(value))
7884

85+
return json unless @options.fetch(:html_safe, true)
86+
7987
# Rails does more escaping than the JSON gem natively does (we
8088
# escape \u2028 and \u2029 and optionally >, <, & to work around
8189
# certain browser problems).
@@ -162,6 +170,8 @@ def encode(value)
162170

163171
json = CODER.dump(value)
164172

173+
return json unless @options.fetch(:html_safe, true)
174+
165175
# Rails does more escaping than the JSON gem natively does (we
166176
# escape \u2028 and \u2029 and optionally >, <, & to work around
167177
# certain browser problems).

0 commit comments

Comments
 (0)