racknga 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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