racknga 0.9.1 → 0.9.2

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.
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
3
+ # Copyright (C) 2010-2011 Kouhei Sutou <kou@clear-code.com>
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -29,21 +29,50 @@ module Racknga
29
29
  class ExceptionMailNotifier
30
30
  def initialize(options)
31
31
  @options = Utils.normalize_options(options || {})
32
+ reset_limitation
32
33
  end
33
34
 
34
35
  def notify(exception, environment)
35
- host = @options[:host] || "localhost"
36
36
  return if to.empty?
37
- mail = format(exception, environment)
37
+
38
+ if limitation_expired?
39
+ send_summaries unless @summaries.empty?
40
+ reset_limitation
41
+ end
42
+
43
+ if @mail_count < max_mail_count_in_limit_duration
44
+ send_notification(exception, environment)
45
+ else
46
+ @summaries << summarize(exception, environment)
47
+ end
48
+
49
+ @mail_count += 1
50
+ end
51
+
52
+ private
53
+ def reset_limitation
54
+ @mail_count = 0
55
+ @count_start_time = Time.now
56
+ @summaries = []
57
+ end
58
+
59
+ def limitation_expired?
60
+ (Time.now - @count_start_time) > limit_duration
61
+ end
62
+
63
+ def send(mail)
64
+ host = @options[:host] || "localhost"
38
65
  Net::SMTP.start(host, @options[:port]) do |smtp|
39
66
  smtp.send_message(mail, from, *to)
40
67
  end
41
68
  end
42
69
 
43
- private
44
- def format(exception, environment)
45
- header = format_header(exception, environment)
46
- body = format_body(exception, environment)
70
+ def create_mail(options)
71
+ subject = [@options[:subject_label], options[:subject]].compact.join(' ')
72
+ header = header(:subject => subject)
73
+
74
+ body = options[:body]
75
+
47
76
  mail = "#{header}\r\n#{body}"
48
77
  mail.force_encoding("utf-8")
49
78
  begin
@@ -53,24 +82,28 @@ module Racknga
53
82
  mail.force_encoding("ASCII-8BIT")
54
83
  end
55
84
 
56
- def format_header(exception, environment)
85
+ def header(options)
57
86
  <<-EOH
58
87
  MIME-Version: 1.0
59
88
  Content-Type: Text/Plain; charset=#{charset}
60
89
  Content-Transfer-Encoding: #{transfer_encoding}
61
90
  From: #{from}
62
91
  To: #{to.join(', ')}
63
- Subject: #{encode_subject(subject(exception, environment))}
92
+ Subject: #{encode_subject(options[:subject])}
64
93
  Date: #{Time.now.rfc2822}
65
94
  EOH
66
95
  end
67
96
 
68
- def format_body(exception, environment)
97
+ def send_notification(exception, environment)
98
+ mail = create_mail(:subject => exception.to_s,
99
+ :body => notification_body(exception, environment))
100
+ send(mail)
101
+ end
102
+
103
+ def notification_body(exception, environment)
69
104
  request = Rack::Request.new(environment)
70
105
  body = <<-EOB
71
- URL: #{request.url}
72
- --
73
- #{exception.class}: #{exception}
106
+ #{summarize(exception, environment)}
74
107
  --
75
108
  #{exception.backtrace.join("\n")}
76
109
  EOB
@@ -98,8 +131,26 @@ EOE
98
131
  body
99
132
  end
100
133
 
101
- def subject(exception, environment)
102
- [@options[:subject_label], exception.to_s].compact.join(' ')
134
+ def send_summaries
135
+ subject = "summaries of #{@summaries.size} notifications"
136
+ mail = create_mail(:subject => subject,
137
+ :body => report_body)
138
+ send(mail)
139
+ end
140
+
141
+ def report_body
142
+ @summaries[0..10].join("\n\n")
143
+ end
144
+
145
+ def summarize(exception, environment)
146
+ request = Rack::Request.new(environment)
147
+ <<-EOB
148
+ Timestamp: #{Time.now.rfc2822}
149
+ --
150
+ URL: #{request.url}
151
+ --
152
+ #{exception.class}: #{exception}
153
+ EOB
103
154
  end
104
155
 
105
156
  def to
@@ -125,6 +176,16 @@ EOE
125
176
  @options[:charset] || 'utf-8'
126
177
  end
127
178
 
179
+ DEFAULT_MAX_MAIL_COUNT_IN_LIMIT_DURATION = 2
180
+ def max_mail_count_in_limit_duration
181
+ @options[:max_mail_count_in_limit_duration] || DEFAULT_MAX_MAIL_COUNT_IN_LIMIT_DURATION
182
+ end
183
+
184
+ DEFAULT_LIMIT_DURATION = 60 # one minute
185
+ def limit_duration
186
+ @options[:limit_duration] || DEFAULT_LIMIT_DURATION
187
+ end
188
+
128
189
  def transfer_encoding
129
190
  case charset
130
191
  when /\Autf-8\z/i
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
3
+ # Copyright (C) 2010-2011 Kouhei Sutou <kou@clear-code.com>
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -21,7 +21,13 @@ require 'fileutils'
21
21
  require 'groonga'
22
22
 
23
23
  module Racknga
24
+ # This is a log database based on groonga. It is used by
25
+ # Racknga::Middleware::Log.
26
+ #
27
+ # Normally, #purge_old_responses is only used for log
28
+ # maintenance.
24
29
  class LogDatabase
30
+ # @param [String] database_path the path for log database.
25
31
  def initialize(database_path)
26
32
  @database_path = database_path
27
33
  @context = Groonga::Context.new(:encoding => :none)
@@ -45,12 +51,23 @@ module Racknga
45
51
  @database.close
46
52
  end
47
53
 
54
+ # Purges old responses. To clear old logs, you should
55
+ # call this method. All records created before
56
+ # +base_time+ are removed.
57
+ #
58
+ # You can call this method by the different
59
+ # process from your Rack application
60
+ # process. (e.g. cron.) It's multi process safe.
61
+ #
62
+ # @param [Time] base_time the oldest record time to be
63
+ # removed. The default value is 1 day ago.
48
64
  def purge_old_entries(base_time=nil)
49
65
  base_time ||= Time.now - 60 * 60 * 24
50
- entries.select do |record|
66
+ target_entries = entries.select do |record|
51
67
  record.time_stamp < base_time
52
- end.each do |record|
53
- record.key.delete
68
+ end
69
+ target_entries.each do |entry|
70
+ entry.key.delete
54
71
  end
55
72
  end
56
73
 
@@ -16,17 +16,36 @@
16
16
  # License along with this library; if not, write to the Free Software
17
17
  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
18
 
19
- require 'digest/md5'
19
+ require 'digest'
20
20
  require 'yaml'
21
21
  require 'racknga/cache_database'
22
22
 
23
23
  module Racknga
24
24
  module Middleware
25
+ # This is a helper middleware for
26
+ # Racknga::Middleware::Cache.
27
+ #
28
+ # If your Rack application provides different views to
29
+ # mobile user agent and PC user agent in the same URL,
30
+ # this middleware is useful. Your Rack application can
31
+ # has different caches for mobile user agent and PC user
32
+ # agent.
33
+ #
34
+ # This middleware requires jpmobile.
35
+ #
36
+ # Usage:
37
+ # use Racnkga::Middleware::PerUserAgentCache
38
+ # use Racnkga::Middleware::Cache, :database_path => "var/cache/db"
39
+ # run YourApplication
40
+ #
41
+ # @see http://jpmobile-rails.org/ jpmobile
42
+ # @see Racknga::Middleware::Cache
25
43
  class PerUserAgentCache
26
44
  def initialize(application)
27
45
  @application = application
28
46
  end
29
47
 
48
+ # For Rack.
30
49
  def call(environment)
31
50
  mobile = environment["rack.jpmobile"]
32
51
  if mobile
@@ -35,16 +54,47 @@ module Racknga
35
54
  else
36
55
  user_agent_key = "pc"
37
56
  end
38
- key = environment[Cache::KEY_KEY]
39
- environment[Cache::KEY_KEY] = [key, user_agent_key].join(":")
57
+ key = environment[Cache::KEY]
58
+ environment[Cache::KEY] = [key, user_agent_key].join(":")
40
59
  @application.call(environment)
41
60
  end
42
61
  end
43
62
 
63
+ # This is a middleware that provides page cache.
64
+ #
65
+ # This stores page contents into a groonga
66
+ # database. A groonga database can access by multi
67
+ # process. It means that your Rack application processes
68
+ # can share the same cache. For example, Passenger runs
69
+ # your Rack application with multi processes.
70
+ #
71
+ # Cache key is the request URL by default. It can be
72
+ # customized by env[Racknga::Cache::KEY]. For example,
73
+ # Racknga::Middleware::PerUserAgentCache and
74
+ # Racknga::Middleware::JSONP use it.
75
+ #
76
+ # This only caches the following responses:
77
+ # * 200 status response.
78
+ # * text/*, */json, */xml or */*+xml content type response.
79
+ #
80
+ # Usage:
81
+ # use Racnkga::Middleware::Cache, :database_path => "var/cache/db"
82
+ # run YourApplication
83
+ #
84
+ # @see Racknga::Middleware::PerUserAgentCache
85
+ # @see Racknga::Middleware::JSONP
86
+ # @see Racknga::Middleware::Deflater
87
+ # @see Racknga::CacheDatabase
44
88
  class Cache
45
- KEY_KEY = "racknga.cache.key"
46
- START_TIME_KEY = "racknga.cache.start_time"
89
+ KEY = "racknga.cache.key"
90
+ START_TIME = "racknga.cache.start_time"
47
91
 
92
+ # @return [Racknga::CacheDatabase] the database used
93
+ # by this middleware.
94
+ attr_reader :database
95
+
96
+ # @option options [String] :database_path the database
97
+ # path to be stored caches.
48
98
  def initialize(application, options={})
49
99
  @application = application
50
100
  @options = Utils.normalize_options(options || {})
@@ -53,12 +103,13 @@ module Racknga
53
103
  @database = CacheDatabase.new(database_path)
54
104
  end
55
105
 
106
+ # For Rack.
56
107
  def call(environment)
57
108
  request = Rack::Request.new(environment)
58
109
  return @application.call(environment) unless use_cache?(request)
59
110
  age = @database.configuration.age
60
- key = environment[KEY_KEY] || request.fullpath
61
- environment[START_TIME_KEY] = Time.now
111
+ key = normalize_key(environment[KEY] || request.fullpath)
112
+ environment[START_TIME] = Time.now
62
113
  cache = @database.responses
63
114
  record = cache[key]
64
115
  if record and record.age == age
@@ -68,10 +119,12 @@ module Racknga
68
119
  end
69
120
  end
70
121
 
122
+ # ensures creating cache database.
71
123
  def ensure_database
72
124
  @database.ensure_database
73
125
  end
74
126
 
127
+ # close the cache database.
75
128
  def close_database
76
129
  @database.close_database
77
130
  end
@@ -97,6 +150,14 @@ module Racknga
97
150
  true
98
151
  end
99
152
 
153
+ def normalize_key(key)
154
+ if key.size > 4096
155
+ Digest::SHA1.hexdigest(key).force_encoding("ASCII-8BIT")
156
+ else
157
+ key
158
+ end
159
+ end
160
+
100
161
  def handle_request(cache, key, age, request)
101
162
  status, headers, body = @application.call(request.env)
102
163
  if skip_caching_response?(status, headers, body)
@@ -142,13 +203,13 @@ module Racknga
142
203
  end
143
204
 
144
205
  def compute_checksum(status, encoded_headers, encoded_body)
145
- md5 = Digest::MD5.new
146
- md5 << status.to_s
147
- md5 << ":"
148
- md5 << encoded_headers
149
- md5 << ":"
150
- md5 << encoded_body
151
- md5.hexdigest.force_encoding("ASCII-8BIT")
206
+ checksum = Digest::SHA1.new
207
+ checksum << status.to_s
208
+ checksum << ":"
209
+ checksum << encoded_headers
210
+ checksum << ":"
211
+ checksum << encoded_body
212
+ checksum.hexdigest.force_encoding("ASCII-8BIT")
152
213
  end
153
214
 
154
215
  def valid_cache?(status, encoded_headers, encoded_body, checksum)
@@ -18,6 +18,42 @@
18
18
 
19
19
  module Racknga
20
20
  module Middleware
21
+ # This is a middleware that deflates response except for
22
+ # IE6. If your Rack application need support IE6, use
23
+ # this middleware instead of Rack::Deflater.
24
+ #
25
+ # Usage:
26
+ # require "racknga"
27
+ #
28
+ # use Racknga::Middleware::Deflater
29
+ # run YourApplication
30
+ #
31
+ # You can use this middleware with
32
+ # Racknga::Middleware::Cache. You *should* use this
33
+ # middleware before the cache middleware:
34
+ # use Racknga::Middleawre::Deflater
35
+ # use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
36
+ # run YourApplication
37
+ #
38
+ # If you use this middleware after the cache middleware,
39
+ # you get two problems. It's the first problem pattern
40
+ # that the cache middleware may return deflated response
41
+ # to IE6. It's the second problem pattern that the cache
42
+ # middleware may return not deflated response to no IE6
43
+ # user agent. Here are examples:
44
+ #
45
+ # Problem case:
46
+ # use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
47
+ # use Racknga::Middleawre::Deflater
48
+ # run YourApplication
49
+ #
50
+ # Problem pattern1:
51
+ # http://localhost:9292/ by Firefox -> no cache. cache deflated response.
52
+ # http://localhost:9292/ by IE6 -> use deflated response cache.
53
+ #
54
+ # Problem pattern2:
55
+ # http://localhost:9292/ by IE6 -> no cache. cache not deflated response.
56
+ # http://localhost:9292/ by Firefox -> use not deflated response cache.
21
57
  class Deflater
22
58
  def initialize(application, options={})
23
59
  @application = application
@@ -25,6 +61,7 @@ module Racknga
25
61
  @options = Utils.normalize_options(options || {})
26
62
  end
27
63
 
64
+ # For Rack.
28
65
  def call(environment)
29
66
  if ie6?(environment)
30
67
  @application.call(environment)
@@ -20,6 +20,22 @@ require 'racknga/exception_mail_notifier'
20
20
 
21
21
  module Racknga
22
22
  module Middleware
23
+ # This is a middleware that mails exception details on
24
+ # error. It's useful for finding your Rack application
25
+ # troubles.
26
+ #
27
+ # Usage:
28
+ # require "racknga"
29
+ # require "racknga/middleware/exception_notifier"
30
+ #
31
+ # notifier_options = {
32
+ # :subject_label => "[YourApplication]",
33
+ # :from => "reporter@example.com",
34
+ # :to => "maintainers@example.com",
35
+ # }
36
+ # notifiers = [Racknga::ExceptionMailNotifier.new(notifier_options)]
37
+ # use Racknga::Middleware::ExceptionNotifier, :notifiers => notifiers
38
+ # run YourApplication
23
39
  class ExceptionNotifier
24
40
  def initialize(application, options={})
25
41
  @application = application
@@ -27,6 +43,7 @@ module Racknga
27
43
  @notifiers = @options[:notifiers] || []
28
44
  end
29
45
 
46
+ # For Rack.
30
47
  def call(environment)
31
48
  @application.call(environment)
32
49
  rescue Exception => exception
@@ -0,0 +1,134 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2011 Ryo Onodera <onodera@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
+
19
+ module Racknga
20
+ module Middleware
21
+ # This is a middleware that adds "X-Responsed-By" header
22
+ # to responses. It's useful to determine responded
23
+ # server when your Rack applications are deployed behind
24
+ # load balancers.
25
+ #
26
+ # Usage:
27
+ # require "racknga"
28
+ # use Racknga::Middleware::InstanceName
29
+ # run YourApplication
30
+ class InstanceName
31
+ attr_reader :header
32
+ def initialize(application, options={})
33
+ @application = application
34
+ @options = options
35
+
36
+ @header = construct_header.freeze
37
+ @headers = construct_headers.freeze
38
+ end
39
+
40
+ # For Rack.
41
+ def call(environment)
42
+ response = @application.call(environment).to_a
43
+
44
+ [
45
+ response[0],
46
+ response[1].merge(@headers),
47
+ response[2],
48
+ ]
49
+ end
50
+
51
+ def application_name
52
+ @options[:application_name] || @application.class.name
53
+ end
54
+
55
+ def version
56
+ @options[:version]
57
+ end
58
+
59
+ def revision
60
+ `git describe --abbrev=7 HEAD`.strip # XXX be SCM-agonostic
61
+ end
62
+
63
+ def server
64
+ `hostname`.strip
65
+ end
66
+
67
+ def user
68
+ `id --user --name`.strip
69
+ end
70
+
71
+ private
72
+ DEFAULT_HEADER_NAME = "X-Responsed-By"
73
+ def header_name
74
+ @options[:header_name] || DEFAULT_HEADER_NAME
75
+ end
76
+
77
+ def construct_headers
78
+ {
79
+ header_name => header,
80
+ }
81
+ end
82
+
83
+ def construct_header
84
+ format_header(format_application_name(application_name),
85
+ format_version(version),
86
+ format_revision(revision),
87
+ format_server(server),
88
+ format_user(user))
89
+ end
90
+
91
+ def format_header(*arguments)
92
+ arguments.compact.join(" ")
93
+ end
94
+
95
+ def format_application_name(name)
96
+ format_if_possible(name) do
97
+ "#{name}"
98
+ end
99
+ end
100
+
101
+ def format_version(version)
102
+ format_if_possible(version) do
103
+ "v#{version}"
104
+ end
105
+ end
106
+
107
+ def format_revision(revision)
108
+ format_if_possible(revision) do
109
+ "(at #{revision})"
110
+ end
111
+ end
112
+
113
+ def format_server(server)
114
+ format_if_possible(server) do
115
+ "on #{server}"
116
+ end
117
+ end
118
+
119
+ def format_user(user)
120
+ format_if_possible(user) do
121
+ "by #{user}"
122
+ end
123
+ end
124
+
125
+ def format_if_possible(data)
126
+ if data and (data.respond_to?(:to_s) and not data.to_s.empty?)
127
+ result = yield
128
+ end
129
+
130
+ result
131
+ end
132
+ end
133
+ end
134
+ end
@@ -18,26 +18,87 @@
18
18
 
19
19
  module Racknga
20
20
  module Middleware
21
+ # This is a middleware that provides JSONP support.
22
+ #
23
+ # If you use this middleware, your Rack application just
24
+ # returns JSON response.
25
+ #
26
+ # Usage:
27
+ # require "racknga"
28
+ #
29
+ # use Rack::ContentLength
30
+ # use Racknga::Middleware::JSONP
31
+ # json_application = Proc.new do |env|
32
+ # [200,
33
+ # {"Content-Type" => "application/json"},
34
+ # ['{"Hello": "World"}']]
35
+ # end
36
+ # run json_application
37
+ #
38
+ # Results:
39
+ # % curl 'http://localhost:9292/'
40
+ # {"Hello": "World"}
41
+ # % curl 'http://localhost:9292/?callback=function'
42
+ # function({"Hello": "World"})
43
+ #
44
+ # You can use this middleware with
45
+ # Racknga::Middleware::Cache. You *should* use this
46
+ # middleware before the cache middleware:
47
+ # use Racknga::Middleawre::JSONP
48
+ # use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
49
+ # run YourApplication
50
+ #
51
+ # If you use this middleware after the cache middleware,
52
+ # the cache middleware will cache many responses that
53
+ # just only differ callback parameter value. Here are
54
+ # examples:
55
+ #
56
+ # Recommended case:
57
+ # use Racknga::Middleawre::JSONP
58
+ # use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
59
+ # run YourApplication
60
+ #
61
+ # Requests:
62
+ # http://localhost:9292/ -> no cache. cached.
63
+ # http://localhost:9292/?callback=function1 -> use cache.
64
+ # http://localhost:9292/?callback=function2 -> use cache.
65
+ # http://localhost:9292/?callback=function3 -> use cache.
66
+ # http://localhost:9292/?callback=function1 -> use cache.
67
+ #
68
+ # Not recommended case:
69
+ # use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
70
+ # use Racknga::Middleawre::JSONP
71
+ # run YourApplication
72
+ #
73
+ # Requests:
74
+ # http://localhost:9292/ -> no cache. cached.
75
+ # http://localhost:9292/?callback=function1 -> no cache. cached.
76
+ # http://localhost:9292/?callback=function2 -> no cache. cached.
77
+ # http://localhost:9292/?callback=function3 -> no cache. cached.
78
+ # http://localhost:9292/?callback=function1 -> use cache.
21
79
  class JSONP
22
80
  def initialize(application)
23
81
  @application = application
24
82
  end
25
83
 
84
+ # For Rack.
26
85
  def call(environment)
27
86
  request = Rack::Request.new(environment)
28
87
  callback = request["callback"]
29
88
  update_cache_key(request) if callback
30
89
  status, headers, body = @application.call(environment)
31
90
  return [status, headers, body] unless callback
32
- return [status, headers, body] unless json_response?(headers)
91
+ header_hash = Rack::Utils::HeaderHash.new(headers)
92
+ return [status, headers, body] unless json_response?(header_hash)
33
93
  body = Writer.new(callback, body)
34
- [status, headers, body]
94
+ update_content_type(header_hash)
95
+ [status, header_hash, body]
35
96
  end
36
97
 
37
98
  private
38
99
  def update_cache_key(request)
39
100
  return unless Middleware.const_defined?(:Cache)
40
- cache_key_key = Cache::KEY_KEY
101
+ cache_key = Cache::KEY
41
102
 
42
103
  path = request.fullpath
43
104
  path, parameters = path.split(/\?/, 2)
@@ -49,17 +110,28 @@ module Racknga
49
110
  path << "?" << parameters unless parameters.empty?
50
111
  end
51
112
 
52
- key = request.env[cache_key_key]
53
- request.env[cache_key_key] = [key, path].compact.join(":")
113
+ key = request.env[cache_key]
114
+ request.env[cache_key] = [key, path].compact.join(":")
54
115
  end
55
116
 
56
- def json_response?(headers)
57
- content_type = Rack::Utils::HeaderHash.new(headers)["Content-Type"]
58
- content_type == "application/json" or
59
- content_type == "application/javascript" or
60
- content_type == "text/javascript"
117
+ def json_response?(header_hash)
118
+ content_type = header_hash["Content-Type"]
119
+ media_type = content_type.split(/\s*;\s*/, 2).first.downcase
120
+ media_type == "application/json" or
121
+ media_type == "application/javascript" or
122
+ media_type == "text/javascript"
61
123
  end
62
124
 
125
+ def update_content_type(header_hash)
126
+ content_type = header_hash["Content-Type"]
127
+ media_type, parameters = content_type.split(/\s*;\s*/, 2)
128
+ # We should use application/javascript not
129
+ # text/javascript when all IE <= 8 are deprecated. :<
130
+ updated_content_type = ["text/javascript", parameters].compact.join("; ")
131
+ header_hash["Content-Type"] = updated_content_type
132
+ end
133
+
134
+ # @private
63
135
  class Writer
64
136
  def initialize(callback, body)
65
137
  @callback = callback