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