roda 3.12.0 → 3.13.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/CHANGELOG +8 -0
- data/doc/release_notes/3.13.0.txt +38 -0
- data/lib/roda/plugins/common_logger.rb +2 -0
- data/lib/roda/plugins/exception_page.rb +405 -0
- data/lib/roda/plugins/streaming.rb +9 -3
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/common_logger_spec.rb +26 -0
- data/spec/plugin/exception_page_spec.rb +157 -0
- data/spec/plugin/streaming_spec.rb +18 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5b811d1ec0ddceb3a222c490b2af8e1488c6d454b637c7b6522962c149e6fd6
|
|
4
|
+
data.tar.gz: 0f3fad8d3d14f799fcc0e92a29bbc9210aadec3bade4bf68379409ce8780b29a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8de657deae4c712c6d18c329c1137e103b979d7c1c4e1f7a36fa9a72f3bd0c202610bc73e78b7ce30c80806adfcd19a67d42da0ce5ab4932e04a8df6aba7a54d
|
|
7
|
+
data.tar.gz: 66cc1c59eca2bc0fb7a5186fc353332ca630b255de48406429fbb1ecfb7a42253102d2616b98196c8e4bd608a335ea2ce7af973b5b15c6150fddfc124b7b7591
|
data/CHANGELOG
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
= 3.13.0 (2018-10-12)
|
|
2
|
+
|
|
3
|
+
* Make Stream#write in streaming plugin return number of bytes written instead of self, so it works with IO.copy_stream (jeremyevans)
|
|
4
|
+
|
|
5
|
+
* Add exception_page plugin for showing a page with debugging information for a given exception (jeremyevans)
|
|
6
|
+
|
|
7
|
+
* Make common_logger plugin handle raised errors (jeremyevans)
|
|
8
|
+
|
|
1
9
|
= 3.12.0 (2018-09-14)
|
|
2
10
|
|
|
3
11
|
* Add common_logger plugin for common log support (jeremyevans)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
= New Features
|
|
2
|
+
|
|
3
|
+
* An exception_page plugin has been added for displaying debugging
|
|
4
|
+
information for a given exception. It is based on
|
|
5
|
+
Rack::ShowExceptions, with the following differences:
|
|
6
|
+
|
|
7
|
+
* Not a middleware, so it doesn't handle exceptions itself, and
|
|
8
|
+
has no effect on the callstack unless the exception_page
|
|
9
|
+
method is called.
|
|
10
|
+
* Supports external javascript and stylesheets, allowing context
|
|
11
|
+
toggling to work in applications that use a content security
|
|
12
|
+
policy to restrict inline javascript and stylesheets (:assets,
|
|
13
|
+
:css_file, and :js_file options).
|
|
14
|
+
* Has fewer dependencies (does not require ostruct and erb).
|
|
15
|
+
* Sets the Content-Type for the response, and returns the body
|
|
16
|
+
string, but does not modify other headers or the response status.
|
|
17
|
+
* Supports a configurable amount of context lines in backtraces
|
|
18
|
+
(:context option).
|
|
19
|
+
* Supports optional JSON formatted output, if used with the json
|
|
20
|
+
plugin (:json option).
|
|
21
|
+
|
|
22
|
+
Because this plugin just adds a method you can call, you can
|
|
23
|
+
selectively choose when to display a debugging page and when not
|
|
24
|
+
to, as well as customize the debugging parameters on a per-call
|
|
25
|
+
basis (such as returning JSON formatted debugging information
|
|
26
|
+
for JSON requests, and HTML formatted debugging information for
|
|
27
|
+
normal requests).
|
|
28
|
+
|
|
29
|
+
= Other Improvements
|
|
30
|
+
|
|
31
|
+
* The common_logger plugin now correctly handles cases where an
|
|
32
|
+
exception is being raised and there is no rack response to
|
|
33
|
+
introspect.
|
|
34
|
+
|
|
35
|
+
= Backwards Compatibility
|
|
36
|
+
|
|
37
|
+
* Stream#write in the streaming plugin now returns the number of
|
|
38
|
+
bytes written instead of self, so it works with IO.copy_stream.
|
|
@@ -47,6 +47,8 @@ class Roda
|
|
|
47
47
|
|
|
48
48
|
# Log request/response information in common log format to logger.
|
|
49
49
|
def _roda_after_90__common_logger(result)
|
|
50
|
+
return unless result && result[0] && result[1]
|
|
51
|
+
|
|
50
52
|
elapsed_time = if timer = @_request_timer
|
|
51
53
|
'%0.4f' % (CommonLogger.start_timer - timer)
|
|
52
54
|
else
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
# The exception_page plugin provides an exception_page method that is designed
|
|
7
|
+
# to be called inside the error handler to provide a page to the developer
|
|
8
|
+
# with debugging information. It should only be used in developer environments
|
|
9
|
+
# with trusted clients, as it can leak source code and other information that
|
|
10
|
+
# may be useful for attackers if used in other environments.
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
#
|
|
14
|
+
# plugin :exception_page
|
|
15
|
+
# plugin :error_handler do |e|
|
|
16
|
+
# next exception_page(e) if ENV['RACK_ENV'] == 'development'
|
|
17
|
+
# # ...
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# The exception_page plugin is based on Rack::ShowExceptions, with the following
|
|
21
|
+
# differences:
|
|
22
|
+
#
|
|
23
|
+
# * Not a middleware, so it doesn't handle exceptions itself, and has no effect
|
|
24
|
+
# on the callstack unless the exception_page method is called.
|
|
25
|
+
# * Supports external javascript and stylesheets, allowing context toggling to
|
|
26
|
+
# work in applications that use a content security policy to restrict inline
|
|
27
|
+
# javascript and stylesheets (:assets, :css_file, and :js_file options).
|
|
28
|
+
# * Has fewer dependencies (does not require ostruct and erb).
|
|
29
|
+
# * Sets the Content-Type for the response, and returns the body string, but does
|
|
30
|
+
# not modify other headers or the response status.
|
|
31
|
+
# * Supports a configurable amount of context lines in backtraces (:context option).
|
|
32
|
+
# * Supports optional JSON formatted output, if used with the json plugin (:json option).
|
|
33
|
+
#
|
|
34
|
+
# To use the external javascript and stylesheets, you can call +r.exception_page_assets+
|
|
35
|
+
# in your routing tree:
|
|
36
|
+
#
|
|
37
|
+
# route do |r|
|
|
38
|
+
# # ...
|
|
39
|
+
#
|
|
40
|
+
# # serve GET /exception_page.{css,js} requests
|
|
41
|
+
# # Use with assets: true +exception_page+ option
|
|
42
|
+
# r.exception_page_assets
|
|
43
|
+
#
|
|
44
|
+
# r.on "static" do
|
|
45
|
+
# # serve GET /static/exception_page.{css,js} requests
|
|
46
|
+
# # Use with assets: '/static' +exception_page+ option
|
|
47
|
+
# r.exception_page_assets
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# It's also possible to store the asset information in static files and serve those,
|
|
52
|
+
# you can get the current assets by calling:
|
|
53
|
+
#
|
|
54
|
+
# Roda::RodaPlugins::ExceptionPage.css
|
|
55
|
+
# Roda::RodaPlugins::ExceptionPage.js
|
|
56
|
+
#
|
|
57
|
+
# As the exception_page plugin is based on Rack::ShowExceptions, it is also under
|
|
58
|
+
# rack's license:
|
|
59
|
+
#
|
|
60
|
+
# Copyright (C) 2007-2018 Christian Neukirchen <http://chneukirchen.org/infopage.html>
|
|
61
|
+
#
|
|
62
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
63
|
+
# of this software and associated documentation files (the "Software"), to
|
|
64
|
+
# deal in the Software without restriction, including without limitation the
|
|
65
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
66
|
+
# sell copies of the Software, and to permit persons to whom the Software is
|
|
67
|
+
# furnished to do so, subject to the following conditions:
|
|
68
|
+
#
|
|
69
|
+
# The above copyright notice and this permission notice shall be included in
|
|
70
|
+
# all copies or substantial portions of the Software.
|
|
71
|
+
#
|
|
72
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
73
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
74
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
75
|
+
# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
76
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
77
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
78
|
+
#
|
|
79
|
+
# The HTML template used in Rack::ShowExceptions was based on Django's
|
|
80
|
+
# template and is under the following license:
|
|
81
|
+
#
|
|
82
|
+
# adapted from Django <www.djangoproject.com>
|
|
83
|
+
# Copyright (c) Django Software Foundation and individual contributors.
|
|
84
|
+
# Used under the modified BSD license:
|
|
85
|
+
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
|
|
86
|
+
module ExceptionPage
|
|
87
|
+
def self.load_dependencies(app)
|
|
88
|
+
app.plugin :h
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Stylesheet used by the HTML exception page
|
|
92
|
+
def self.css
|
|
93
|
+
<<END
|
|
94
|
+
html * { padding:0; margin:0; }
|
|
95
|
+
body * { padding:10px 20px; }
|
|
96
|
+
body * * { padding:0; }
|
|
97
|
+
body { font:small sans-serif; }
|
|
98
|
+
body>div { border-bottom:1px solid #ddd; }
|
|
99
|
+
h1 { font-weight:normal; }
|
|
100
|
+
h2 { margin-bottom:.8em; }
|
|
101
|
+
h2 span { font-size:80%; color:#666; font-weight:normal; }
|
|
102
|
+
h3 { margin:1em 0 .5em 0; }
|
|
103
|
+
h4 { margin:0 0 .5em 0; font-weight: normal; }
|
|
104
|
+
table {
|
|
105
|
+
border:1px solid #ccc; border-collapse: collapse; background:white; }
|
|
106
|
+
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
|
|
107
|
+
thead th {
|
|
108
|
+
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
|
|
109
|
+
font-weight:normal; font-size:11px; border:1px solid #ddd; }
|
|
110
|
+
tbody th { text-align:right; color:#666; padding-right:.5em; }
|
|
111
|
+
table.vars { margin:5px 0 2px 40px; }
|
|
112
|
+
table.vars td, table.req td { font-family:monospace; }
|
|
113
|
+
table td.code { width:100%;}
|
|
114
|
+
table td.code div { overflow:hidden; }
|
|
115
|
+
table.source th { color:#666; }
|
|
116
|
+
table.source td {
|
|
117
|
+
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
|
|
118
|
+
ul.traceback { list-style-type:none; }
|
|
119
|
+
ul.traceback li.frame { margin-bottom:1em; }
|
|
120
|
+
div.context { margin: 10px 0; }
|
|
121
|
+
div.context ol {
|
|
122
|
+
padding-left:30px; margin:0 10px; list-style-position: inside; }
|
|
123
|
+
div.context ol li {
|
|
124
|
+
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
|
|
125
|
+
div.context ol.context-line li { color:black; background-color:#ccc; }
|
|
126
|
+
div.context ol.context-line li span { float: right; }
|
|
127
|
+
div.commands { margin-left: 40px; }
|
|
128
|
+
div.commands a { color:black; text-decoration:none; }
|
|
129
|
+
#summary { background: #ffc; }
|
|
130
|
+
#summary h2 { font-weight: normal; color: #666; }
|
|
131
|
+
#summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
|
|
132
|
+
#summary ul#quicklinks li { float: left; padding: 0 1em; }
|
|
133
|
+
#summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
|
|
134
|
+
#explanation { background:#eee; }
|
|
135
|
+
#traceback { background:#eee; }
|
|
136
|
+
#requestinfo { background:#f6f6f6; padding-left:120px; }
|
|
137
|
+
#summary table { border:none; background:transparent; }
|
|
138
|
+
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
|
|
139
|
+
#requestinfo h3 { margin-bottom:-1em; }
|
|
140
|
+
.error { background: #ffc; }
|
|
141
|
+
.specific { color:#cc3300; font-weight:bold; }
|
|
142
|
+
END
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Javascript used by the HTML exception page for context toggling
|
|
146
|
+
def self.js
|
|
147
|
+
<<END
|
|
148
|
+
var contexts = document.getElementsByClassName('context');
|
|
149
|
+
var num_contexts = contexts.length;
|
|
150
|
+
function toggle() {
|
|
151
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
152
|
+
var e = document.getElementById(arguments[i]);
|
|
153
|
+
if (e) {
|
|
154
|
+
e.style.display = e.style.display == 'none' ? 'block' : 'none';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
for (var j = 0; j < num_contexts; j++) {
|
|
160
|
+
contexts[j].onclick = function(){toggle('b'+this.id, 'a'+this.id);}
|
|
161
|
+
contexts[j].onclick();
|
|
162
|
+
}
|
|
163
|
+
END
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
module InstanceMethods
|
|
167
|
+
# Return a HTML page showing the exception, allowing a developer
|
|
168
|
+
# more information for debugging. Designed to be called inside
|
|
169
|
+
# an exception handler, passing in the received exception.
|
|
170
|
+
# Sets the Content-Type header in the response, and returns the
|
|
171
|
+
# string used for the body. If the Accept request header is present
|
|
172
|
+
# and text/html is accepted, return an HTML page with the backtrace
|
|
173
|
+
# with the ability to see the context for each backtrace line, as
|
|
174
|
+
# well as the GET, POST, cookie, and rack environment data. If text/html
|
|
175
|
+
# is not accepted, then just show a plain text page with the exception
|
|
176
|
+
# class, message, and backtrace.
|
|
177
|
+
#
|
|
178
|
+
# Options:
|
|
179
|
+
#
|
|
180
|
+
# :assets :: If +true+, sets :css_file to +/exception_page.css+ and :js_file to
|
|
181
|
+
# +/exception_page.js+, assuming that +r.exception_page_assets+ is called
|
|
182
|
+
# in the route block to serve the exception page assets. If a String,
|
|
183
|
+
# uses the string as a prefix, assuming that +r.exception_page_assets+
|
|
184
|
+
# is called in a nested block inside the route block. If false, doesn't
|
|
185
|
+
# use any CSS or JS.
|
|
186
|
+
# :context :: The number of context lines before and after each line in
|
|
187
|
+
# the backtrace (default: 7).
|
|
188
|
+
# :css_file :: A path to the external CSS file for the HTML exception page. If false,
|
|
189
|
+
# doesn't use any CSS.
|
|
190
|
+
# :js_file :: A path to the external javascript file for the HTML exception page. If
|
|
191
|
+
# false, doesn't use any JS.
|
|
192
|
+
# :json :: Return a hash of exception information. The hash will have
|
|
193
|
+
# a single key, "exception", with a value being a hash with
|
|
194
|
+
# three keys, "class", "message", and "backtrace", which
|
|
195
|
+
# contain information derived from the given exception.
|
|
196
|
+
# Designed to be used with the +json+ exception, which will
|
|
197
|
+
# automatically convert the hash to JSON format.
|
|
198
|
+
def exception_page(exception, opts=OPTS)
|
|
199
|
+
if opts[:json]
|
|
200
|
+
@_response['Content-Type'] = "application/json"
|
|
201
|
+
{
|
|
202
|
+
"exception"=>{
|
|
203
|
+
"class"=>exception.class.to_s,
|
|
204
|
+
"message"=>exception.message.to_s,
|
|
205
|
+
"backtrace"=>exception.backtrace.map(&:to_s)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
elsif env['HTTP_ACCEPT'] =~ /text\/html/
|
|
209
|
+
@_response['Content-Type'] = "text/html"
|
|
210
|
+
|
|
211
|
+
context = opts[:context] || 7
|
|
212
|
+
css_file = opts[:css_file]
|
|
213
|
+
js_file = opts[:js_file]
|
|
214
|
+
|
|
215
|
+
case prefix = opts[:assets]
|
|
216
|
+
when false
|
|
217
|
+
css_file = false if css_file.nil?
|
|
218
|
+
js_file = false if js_file.nil?
|
|
219
|
+
when nil
|
|
220
|
+
# nothing
|
|
221
|
+
else
|
|
222
|
+
prefix = '' if prefix == true
|
|
223
|
+
css_file ||= "#{prefix}/exception_page.css"
|
|
224
|
+
js_file ||= "#{prefix}/exception_page.js"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
css = case css_file
|
|
228
|
+
when nil
|
|
229
|
+
"<style type=\"text/css\">#{ExceptionPage.css}</style>"
|
|
230
|
+
when false
|
|
231
|
+
# :nothing
|
|
232
|
+
else
|
|
233
|
+
"<link rel=\"stylesheet\" href=\"#{h css_file}\" />"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
js = case js_file
|
|
237
|
+
when nil
|
|
238
|
+
"<script type=\"text/javascript\">\n//<!--\n#{ExceptionPage.js}\n//-->\n</script>"
|
|
239
|
+
when false
|
|
240
|
+
# :nothing
|
|
241
|
+
else
|
|
242
|
+
"<script type=\"text/javascript\" src=\"#{h js_file}\"></script>"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
frames = exception.backtrace.map.with_index do |line, i|
|
|
246
|
+
frame = {:id=>i}
|
|
247
|
+
if line =~ /(.*?):(\d+)(:in `(.*)')?/
|
|
248
|
+
filename = frame[:filename] = $1
|
|
249
|
+
lineno = frame[:lineno] = $2.to_i
|
|
250
|
+
frame[:function] = $4
|
|
251
|
+
|
|
252
|
+
begin
|
|
253
|
+
lineno -= 1
|
|
254
|
+
lines = ::File.readlines(filename)
|
|
255
|
+
pre_lineno = frame[:pre_context_lineno] = [lineno-context, 0].max
|
|
256
|
+
frame[:pre_context] = lines[pre_lineno...lineno]
|
|
257
|
+
frame[:context_line] = lines[lineno].chomp
|
|
258
|
+
post_lineno = frame[:post_context_lineno] = [lineno+context, lines.size].min
|
|
259
|
+
frame[:post_context] = lines[lineno+1..post_lineno]
|
|
260
|
+
rescue
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
frame
|
|
264
|
+
else
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
end.compact
|
|
268
|
+
|
|
269
|
+
r = @_request
|
|
270
|
+
info = lambda do |title, id, var, none|
|
|
271
|
+
<<END
|
|
272
|
+
<h3 id="#{id}">#{title}</h3>
|
|
273
|
+
#{(var && !var.empty?) ? (<<END1) : "<p>#{none}</p>"
|
|
274
|
+
<table class="req">
|
|
275
|
+
<thead>
|
|
276
|
+
<tr>
|
|
277
|
+
<th>Variable</th>
|
|
278
|
+
<th>Value</th>
|
|
279
|
+
</tr>
|
|
280
|
+
</thead>
|
|
281
|
+
<tbody>
|
|
282
|
+
#{var.sort_by{|k, _| k.to_s}.map{|key, val| (<<END2)}.join
|
|
283
|
+
<tr>
|
|
284
|
+
<td>#{h key}</td>
|
|
285
|
+
<td class="code"><div>#{h val.inspect}</div></td>
|
|
286
|
+
</tr>
|
|
287
|
+
END2
|
|
288
|
+
}
|
|
289
|
+
</tbody>
|
|
290
|
+
</table>
|
|
291
|
+
END1
|
|
292
|
+
}
|
|
293
|
+
END
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
<<END
|
|
297
|
+
<!DOCTYPE html>
|
|
298
|
+
<html lang="en">
|
|
299
|
+
<head>
|
|
300
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
|
301
|
+
<title>#{h exception.class} at #{h r.path}</title>
|
|
302
|
+
#{css}
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
|
|
306
|
+
<div id="summary">
|
|
307
|
+
<h1>#{h exception.class} at #{h r.path}</h1>
|
|
308
|
+
<h2>#{h exception.message}</h2>
|
|
309
|
+
<table><tr>
|
|
310
|
+
<th>Ruby</th>
|
|
311
|
+
<td>
|
|
312
|
+
#{(first = frames.first) ? "<code>#{h first[:filename]}</code>: in <code>#{h first[:function]}</code>, line #{first[:lineno]}" : "unknown location"}
|
|
313
|
+
</td>
|
|
314
|
+
</tr><tr>
|
|
315
|
+
<th>Web</th>
|
|
316
|
+
<td><code>#{r.request_method} #{h r.host}#{h r.path}</code></td>
|
|
317
|
+
</tr></table>
|
|
318
|
+
|
|
319
|
+
<h3>Jump to:</h3>
|
|
320
|
+
<ul id="quicklinks">
|
|
321
|
+
<li><a href="#get-info">GET</a></li>
|
|
322
|
+
<li><a href="#post-info">POST</a></li>
|
|
323
|
+
<li><a href="#cookie-info">Cookies</a></li>
|
|
324
|
+
<li><a href="#env-info">ENV</a></li>
|
|
325
|
+
</ul>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div id="traceback">
|
|
329
|
+
<h2>Traceback <span>(innermost first)</span></h2>
|
|
330
|
+
<ul class="traceback">
|
|
331
|
+
#{frames.map{|frame| id = frame[:id]; (<<END1)}.join
|
|
332
|
+
<li class="frame">
|
|
333
|
+
<code>#{h frame[:filename]}</code>: in <code>#{h frame[:function]}</code>
|
|
334
|
+
|
|
335
|
+
#{frame[:context_line] ? (<<END2) : '</li>'
|
|
336
|
+
<div class="context" id="c#{id}">
|
|
337
|
+
#{frame[:pre_context] ? (<<END3) : ''
|
|
338
|
+
<ol start="#{frame[:pre_context_lineno]+1}" id="bc#{id}">
|
|
339
|
+
#{frame[:pre_context].map{|line| "<li>#{h line}</li>"}.join}
|
|
340
|
+
</ol>
|
|
341
|
+
END3
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
<ol start="#{frame[:lineno]}" class="context-line">
|
|
345
|
+
<li>#{h frame[:context_line]}<span>...</span></li>
|
|
346
|
+
</ol>
|
|
347
|
+
|
|
348
|
+
#{frame[:post_context] ? (<<END4) : ''
|
|
349
|
+
<ol start='#{frame[:lineno]+1}' id="ac#{id}">
|
|
350
|
+
#{frame[:post_context].map{|line| "<li>#{h line}</li>"}.join}
|
|
351
|
+
</ol>
|
|
352
|
+
END4
|
|
353
|
+
}
|
|
354
|
+
</div>
|
|
355
|
+
END2
|
|
356
|
+
}
|
|
357
|
+
END1
|
|
358
|
+
}
|
|
359
|
+
</ul>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<div id="requestinfo">
|
|
363
|
+
<h2>Request information</h2>
|
|
364
|
+
|
|
365
|
+
#{info.call('GET', 'get-info', r.GET, 'No GET data')}
|
|
366
|
+
#{info.call('POST', 'post-info', r.POST, 'No POST data')}
|
|
367
|
+
#{info.call('Cookies', 'cookie-info', r.cookies, 'No cookie data')}
|
|
368
|
+
#{info.call('Rack ENV', 'env-info', r.env, 'No Rack env?')}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div id="explanation">
|
|
372
|
+
<p>
|
|
373
|
+
You're seeing this error because you use the Roda exception_page plugin.
|
|
374
|
+
</p>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
#{js}
|
|
378
|
+
</body>
|
|
379
|
+
</html>
|
|
380
|
+
END
|
|
381
|
+
else
|
|
382
|
+
@_response['Content-Type'] = "text/plain"
|
|
383
|
+
"#{exception.class}: #{exception.message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
module RequestMethods
|
|
389
|
+
# Serve exception page assets
|
|
390
|
+
def exception_page_assets
|
|
391
|
+
get 'exception_page.css' do
|
|
392
|
+
response['Content-Type'] = "text/css"
|
|
393
|
+
ExceptionPage.css
|
|
394
|
+
end
|
|
395
|
+
get 'exception_page.js' do
|
|
396
|
+
response['Content-Type'] = "application/javascript"
|
|
397
|
+
ExceptionPage.js
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
register_plugin(:exception_page, ExceptionPage)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
@@ -53,12 +53,18 @@ class Roda
|
|
|
53
53
|
@closed = false
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Add output to the streaming response body.
|
|
56
|
+
# Add output to the streaming response body. Returns number of bytes written.
|
|
57
57
|
def write(data)
|
|
58
|
-
|
|
58
|
+
data = data.to_s
|
|
59
|
+
@out.call(data)
|
|
60
|
+
data.bytesize
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Add output to the streaming response body. Returns self.
|
|
64
|
+
def <<(data)
|
|
65
|
+
write(data)
|
|
59
66
|
self
|
|
60
67
|
end
|
|
61
|
-
alias << write
|
|
62
68
|
|
|
63
69
|
# If not already closed, close the connection, and call
|
|
64
70
|
# any callbacks.
|
data/lib/roda/version.rb
CHANGED
|
@@ -56,4 +56,30 @@ describe "common_logger plugin" do
|
|
|
56
56
|
@logger.rewind
|
|
57
57
|
@logger.read.must_match /\A- - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/ " 500 - 0.\d\d\d\d\n\z/
|
|
58
58
|
end
|
|
59
|
+
|
|
60
|
+
it 'does not log if an error is raised' do
|
|
61
|
+
cl_app do |r|
|
|
62
|
+
raise "foo"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
body
|
|
67
|
+
rescue => e
|
|
68
|
+
end
|
|
69
|
+
e.must_be_instance_of(RuntimeError)
|
|
70
|
+
e.message.must_equal 'foo'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'logs errors if used with error_handler' do
|
|
74
|
+
cl_app do |r|
|
|
75
|
+
raise "foo"
|
|
76
|
+
end
|
|
77
|
+
@app.plugin :error_handler do |_|
|
|
78
|
+
"bad"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
body.must_equal 'bad'
|
|
82
|
+
@logger.rewind
|
|
83
|
+
@logger.read.must_match /\A- - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/ " 500 3 0.\d\d\d\d\n\z/
|
|
84
|
+
end
|
|
59
85
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require_relative "../spec_helper"
|
|
2
|
+
|
|
3
|
+
describe "exception_page plugin" do
|
|
4
|
+
def ep_app(&block)
|
|
5
|
+
app(:exception_page) do |r|
|
|
6
|
+
raise "foo" rescue block ? instance_exec($!, &block) : exception_page($!)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def req(path = '/', headers={})
|
|
11
|
+
if path.is_a?(Hash)
|
|
12
|
+
super(path.merge('rack.input'=>StringIO.new))
|
|
13
|
+
else
|
|
14
|
+
super(path, headers.merge('rack.input'=>StringIO.new))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns HTML page with exception information if text/html is accepted" do
|
|
19
|
+
ep_app
|
|
20
|
+
s, h, body = req('HTTP_ACCEPT'=>'text/html')
|
|
21
|
+
|
|
22
|
+
s.must_equal 200
|
|
23
|
+
h['Content-Type'].must_equal 'text/html'
|
|
24
|
+
body = body.join
|
|
25
|
+
body.must_include "<title>RuntimeError at /"
|
|
26
|
+
body.must_include "<h1>RuntimeError at /</h1>"
|
|
27
|
+
body.must_include "<h2>foo</h2>"
|
|
28
|
+
body.must_include __FILE__
|
|
29
|
+
body.must_include "No GET data"
|
|
30
|
+
body.must_include "No POST data"
|
|
31
|
+
body.must_include "No cookie data"
|
|
32
|
+
body.must_include "Rack ENV"
|
|
33
|
+
body.must_include "HTTP_ACCEPT"
|
|
34
|
+
body.must_include "text/html"
|
|
35
|
+
body.must_include "table td.code"
|
|
36
|
+
body.must_include "function toggle()"
|
|
37
|
+
body.wont_include "\"/exception_page.css\""
|
|
38
|
+
body.wont_include "\"/exception_page.js\""
|
|
39
|
+
|
|
40
|
+
size = body.size
|
|
41
|
+
ep_app{|e| exception_page(e, :context=>10)}
|
|
42
|
+
body('HTTP_ACCEPT'=>'text/html').size.must_be :>, size
|
|
43
|
+
|
|
44
|
+
ep_app{|e| exception_page(e, :assets=>true, :context=>0)}
|
|
45
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
46
|
+
body.wont_include "table td.code"
|
|
47
|
+
body.wont_include "function toggle()"
|
|
48
|
+
body.must_include "\"/exception_page.css\""
|
|
49
|
+
body.must_include "\"/exception_page.js\""
|
|
50
|
+
|
|
51
|
+
ep_app{|e| exception_page(e, :assets=>"/static", :context=>0)}
|
|
52
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
53
|
+
body.wont_include "table td.code"
|
|
54
|
+
body.wont_include "function toggle()"
|
|
55
|
+
body.must_include "\"/static/exception_page.css\""
|
|
56
|
+
body.must_include "\"/static/exception_page.js\""
|
|
57
|
+
|
|
58
|
+
ep_app{|e| exception_page(e, :css_file=>"/foo.css", :context=>0)}
|
|
59
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
60
|
+
body.wont_include "table td.code"
|
|
61
|
+
body.must_include "function toggle()"
|
|
62
|
+
body.must_include "\"/foo.css\""
|
|
63
|
+
|
|
64
|
+
ep_app{|e| exception_page(e, :js_file=>"/foo.js", :context=>0)}
|
|
65
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
66
|
+
body.must_include "table td.code"
|
|
67
|
+
body.wont_include "function toggle()"
|
|
68
|
+
body.must_include "\"/foo.js\""
|
|
69
|
+
|
|
70
|
+
ep_app{|e| exception_page(e, :assets=>false, :context=>0)}
|
|
71
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
72
|
+
body.wont_include "table td.code"
|
|
73
|
+
body.wont_include "function toggle()"
|
|
74
|
+
body.wont_include "\"/exception_page.css\""
|
|
75
|
+
body.wont_include "\"/exception_page.js\""
|
|
76
|
+
|
|
77
|
+
ep_app{|e| exception_page(e, :assets=>false, :css_file=>"/foo.css", :context=>0)}
|
|
78
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
79
|
+
body.wont_include "table td.code"
|
|
80
|
+
body.wont_include "function toggle()"
|
|
81
|
+
body.must_include "\"/foo.css\""
|
|
82
|
+
|
|
83
|
+
ep_app{|e| exception_page(e, :assets=>false, :js_file=>"/foo.js", :context=>0)}
|
|
84
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
85
|
+
body.wont_include "table td.code"
|
|
86
|
+
body.wont_include "function toggle()"
|
|
87
|
+
body.must_include "\"/foo.js\""
|
|
88
|
+
|
|
89
|
+
ep_app{|e| exception_page(e, :css_file=>false, :context=>0)}
|
|
90
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
91
|
+
body.wont_include "table td.code"
|
|
92
|
+
body.must_include "function toggle()"
|
|
93
|
+
body.wont_include "\"/exception_page.css\""
|
|
94
|
+
body.wont_include "\"/exception_page.js\""
|
|
95
|
+
|
|
96
|
+
ep_app{|e| exception_page(e, :js_file=>false, :context=>0)}
|
|
97
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
98
|
+
body.must_include "table td.code"
|
|
99
|
+
body.wont_include "function toggle()"
|
|
100
|
+
body.wont_include "\"/exception_page.css\""
|
|
101
|
+
body.wont_include "\"/exception_page.js\""
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "returns plain text page with exception information if text/html is not accepted" do
|
|
105
|
+
ep_app
|
|
106
|
+
s, h, body = req
|
|
107
|
+
|
|
108
|
+
s.must_equal 200
|
|
109
|
+
h['Content-Type'].must_equal 'text/plain'
|
|
110
|
+
body = body.join
|
|
111
|
+
first, *bt = body.split("\n")
|
|
112
|
+
first.must_equal "RuntimeError: foo"
|
|
113
|
+
bt.first.must_include __FILE__
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "returns JSON with exception information if :json information is used" do
|
|
117
|
+
ep_app{|e| exception_page(e, :json=>true)}
|
|
118
|
+
@app.plugin :json
|
|
119
|
+
s, h, body = req
|
|
120
|
+
|
|
121
|
+
s.must_equal 200
|
|
122
|
+
h['Content-Type'].must_equal 'application/json'
|
|
123
|
+
hash = JSON.parse(body.join)
|
|
124
|
+
bt = hash["exception"].delete("backtrace")
|
|
125
|
+
hash.must_equal("exception"=>{"class"=>"RuntimeError", "message"=>"foo"})
|
|
126
|
+
bt.must_be_kind_of Array
|
|
127
|
+
bt.each{|line| line.must_be_kind_of String}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "should handle backtrace lines in unexpected forms" do
|
|
131
|
+
ep_app do |e|
|
|
132
|
+
e.backtrace.first.upcase!
|
|
133
|
+
e.backtrace[-1] = ''
|
|
134
|
+
exception_page(e)
|
|
135
|
+
end
|
|
136
|
+
body = body('HTTP_ACCEPT'=>'text/html')
|
|
137
|
+
body.must_include "RuntimeError: foo"
|
|
138
|
+
body.must_include __FILE__
|
|
139
|
+
body.wont_include 'id="c0"'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "should serve exception page assets" do
|
|
143
|
+
app(:exception_page) do |r|
|
|
144
|
+
r.exception_page_assets
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
s, h, b = req('/exception_page.css')
|
|
148
|
+
s.must_equal 200
|
|
149
|
+
h['Content-Type'].must_equal 'text/css'
|
|
150
|
+
b.join.must_equal Roda::RodaPlugins::ExceptionPage.css
|
|
151
|
+
|
|
152
|
+
s, h, b = req('/exception_page.js')
|
|
153
|
+
s.must_equal 200
|
|
154
|
+
h['Content-Type'].must_equal 'application/javascript'
|
|
155
|
+
b.join.must_equal Roda::RodaPlugins::ExceptionPage.js
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -4,7 +4,10 @@ describe "streaming plugin" do
|
|
|
4
4
|
it "adds stream method for streaming responses" do
|
|
5
5
|
app(:streaming) do |r|
|
|
6
6
|
stream do |out|
|
|
7
|
-
%w'a b c'.each
|
|
7
|
+
%w'a b c'.each do |v|
|
|
8
|
+
(out << v).must_equal out
|
|
9
|
+
out.write(v).must_equal 1
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
|
|
@@ -14,6 +17,20 @@ describe "streaming plugin" do
|
|
|
14
17
|
b.to_a.must_equal %w'a a b b c c'
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
it "works with IO.copy_stream" do
|
|
21
|
+
app(:streaming) do |r|
|
|
22
|
+
stream do |out|
|
|
23
|
+
%w'a b c'.each{|v| IO.copy_stream(StringIO.new(v), out) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
s, h, b = req
|
|
28
|
+
s.must_equal 200
|
|
29
|
+
h.must_equal('Content-Type'=>'text/html')
|
|
30
|
+
# dup as copy_stream reuses the buffer
|
|
31
|
+
b.map(&:dup).must_equal %w'a b c'
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
it "should handle errors when streaming, and run callbacks" do
|
|
18
35
|
a = []
|
|
19
36
|
app(:streaming) do |r|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: roda
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Evans
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-
|
|
11
|
+
date: 2018-10-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -208,6 +208,7 @@ extra_rdoc_files:
|
|
|
208
208
|
- doc/release_notes/3.10.0.txt
|
|
209
209
|
- doc/release_notes/3.11.0.txt
|
|
210
210
|
- doc/release_notes/3.12.0.txt
|
|
211
|
+
- doc/release_notes/3.13.0.txt
|
|
211
212
|
files:
|
|
212
213
|
- CHANGELOG
|
|
213
214
|
- MIT-LICENSE
|
|
@@ -254,6 +255,7 @@ files:
|
|
|
254
255
|
- doc/release_notes/3.10.0.txt
|
|
255
256
|
- doc/release_notes/3.11.0.txt
|
|
256
257
|
- doc/release_notes/3.12.0.txt
|
|
258
|
+
- doc/release_notes/3.13.0.txt
|
|
257
259
|
- doc/release_notes/3.2.0.txt
|
|
258
260
|
- doc/release_notes/3.3.0.txt
|
|
259
261
|
- doc/release_notes/3.4.0.txt
|
|
@@ -293,6 +295,7 @@ files:
|
|
|
293
295
|
- lib/roda/plugins/error_email.rb
|
|
294
296
|
- lib/roda/plugins/error_handler.rb
|
|
295
297
|
- lib/roda/plugins/error_mail.rb
|
|
298
|
+
- lib/roda/plugins/exception_page.rb
|
|
296
299
|
- lib/roda/plugins/flash.rb
|
|
297
300
|
- lib/roda/plugins/h.rb
|
|
298
301
|
- lib/roda/plugins/halt.rb
|
|
@@ -394,6 +397,7 @@ files:
|
|
|
394
397
|
- spec/plugin/error_email_spec.rb
|
|
395
398
|
- spec/plugin/error_handler_spec.rb
|
|
396
399
|
- spec/plugin/error_mail_spec.rb
|
|
400
|
+
- spec/plugin/exception_page_spec.rb
|
|
397
401
|
- spec/plugin/flash_spec.rb
|
|
398
402
|
- spec/plugin/h_spec.rb
|
|
399
403
|
- spec/plugin/halt_spec.rb
|